commit 490e85634e517245760ae5ecbdf91b6737fe57ed Author: gallery Date: Fri Sep 11 16:11:09 2026 +0800 精简后的项目:仅保留 api/ui 源码、文档、构建依赖 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c423759 --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# gallery.fukun.net + +基于 [Photoview](https://github.com/photoview/photoview) 定制的个人相册,部署在腾讯云 `62.234.90.54`。 + +--- + +## 访问 + +| 地址 | 说明 | +|------|------| +| `https://gallery.fukun.net` | 管理后台(需登录) | +| `https://cdn.fukun.net/gallery/photos/...` | CDN 公开原图 | + +--- + +## 定制项(与上游 Photoview 的差异) + +- **品牌** — 左上角 FK 书法 logo,页面标题 `Gallery.FuKun.Net` +- **外观** — 纯黑背景 (`#000`),菜单图标改为极简线条 +- **字体** — iA Writer Quattro S + Noto Sans SC(与主站 fukun.net 一致) +- **CDN** — 公开原图走 `cdn.fukun.net`,带 token 的鉴权图片自动切 CDN +- **无 Docker** — Go 二进制 + systemd,SQLite 单文件数据库 +- **人脸识别** — 已关闭(`PHOTOVIEW_DISABLE_FACE_RECOGNITION=1`) + +--- + +## 目录结构 + +``` +/home/gallery/ +├── photoview-src/ # 源码 + 编译产物 +│ ├── api/ +│ │ ├── .env # 运行时环境变量 +│ │ └── photoview # Go 二进制(38MB) +│ └── ui/ +│ └── dist/ # 前端构建产物 +├── photos/ # 相片库根目录 +└── data/ + ├── photoview.db # SQLite 数据库 + └── media-cache/ # 缩略图缓存 +``` + +--- + +## 本地开发 + +> 后端依赖 Linux C 库(dlib/libheif/ImageMagick),无法在 Windows 本地运行。 +> 只能改前端代码,API 指向生产环境。 + +```bash +git clone ssh://git@fukun.net/data/git-repos/gallery.git +cd ui + +cat > .env << EOF +REACT_APP_API_ENDPOINT=https://gallery.fukun.net/api/ +REACT_APP_BUILD_VERSION=dev +REACT_APP_BUILD_DATE=$(date -u +%Y-%m-%d) +REACT_APP_BUILD_COMMIT_SHA=local +EOF + +npm ci +npm run dev # localhost:1234,API 连生产服务器 +``` + +--- + +## 部署到服务器 + +```bash +# 构建前端 +scp -r ui/src ui/public ubuntu@62.234.90.54:/tmp/pv-ui/ +ssh ubuntu@62.234.90.54 ' + sudo cp -r /tmp/pv-ui/* /home/gallery/photoview-src/ui/ + sudo chown -R gallery:gallery /home/gallery/photoview-src/ui + sudo -u gallery bash -c "cd /home/gallery/photoview-src/ui && npm ci && npm run build" +' + +# 构建后端 + 重启 +ssh ubuntu@62.234.90.54 ' + cd /home/gallery/photoview-src/api + export PKG_CONFIG_PATH=/usr/local/im7/lib/pkgconfig CGO_ENABLED=1 + sudo -u gallery go build -o photoview . + sudo systemctl restart photoview +' +``` + +--- + +## 运行时配置 + +**systemd** — `/etc/systemd/system/photoview.service` + +| 环境变量 | 值 | +|----------|-----| +| `PHOTOVIEW_DATABASE_DRIVER` | `sqlite` | +| `PHOTOVIEW_SQLITE_PATH` | `/home/gallery/data/photoview.db` | +| `PHOTOVIEW_LISTEN_IP` | `127.0.0.1` | +| `PHOTOVIEW_LISTEN_PORT` | `8000` | +| `PHOTOVIEW_SERVE_UI` | `1` | +| `PHOTOVIEW_UI_PATH` | `/home/gallery/photoview-src/ui/dist` | +| `PHOTOVIEW_MEDIA_CACHE` | `/home/gallery/data/media-cache` | +| `PHOTOVIEW_DISABLE_FACE_RECOGNITION` | `1` | +| `LD_LIBRARY_PATH` | `/usr/local/im7/lib` | + +**Nginx** — `/etc/nginx/sites-enabled/gallery.fukun.net` + +- `gallery.fukun.net` + `cdn.fukun.net` → HTTPS → `127.0.0.1:8000` +- `/photos/` → `alias /home/gallery/photos/`(CDN 回源口) +- `/assets/` → 缓存 30 天 + +**SSL 证书** — 腾讯云证书,`/etc/nginx/ssl/gallery.fukun.net_*` + +--- + +## 维护 + +```bash +# 查看状态 +sudo systemctl status photoview +sudo journalctl -u photoview -f + +# 重启 +sudo systemctl restart photoview + +# 备份数据库 +sudo cp /home/gallery/data/photoview.db /opt/backup/photoview_$(date +%Y%m%d).db + +# 上传照片 +scp *.jpg ubuntu@62.234.90.54:/home/gallery/photos/ +``` + +--- + +## 仓库 + +``` +ssh://git@fukun.net/data/git-repos/gallery.git +``` + +服务器上 `/data/git-repos/gallery.git`(bare),推送到 `master`。 diff --git a/api/.golangci.yml b/api/.golangci.yml new file mode 100644 index 0000000..8bd83c5 --- /dev/null +++ b/api/.golangci.yml @@ -0,0 +1,98 @@ +version: "2" +run: + tests: false + allow-parallel-runners: true + allow-serial-runners: true +output: + formats: + junit-xml: + path: golang-lint.xml + sarif: + path: golang-lint.sarif + sort-order: + - linter + - severity + - file +linters: + enable: + - asciicheck + - bidichk + - copyloopvar + - cyclop + - decorder + - depguard + - dogsled + - dupl + - dupword + - funlen + - gocheckcompilerdirectives + - gochecknoinits + - gocognit + - goconst + - gocyclo + - godot + - godox + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - grouper + - inamedparam + - interfacebloat + - lll + - maintidx + - misspell + - mnd + - nakedret + - nestif + - nlreturn + - nolintlint + - nosprintfhostport + - prealloc + - predeclared + - promlinter + - tagalign + - usestdlibvars + - whitespace + - wsl + disable: + - unused + exclusions: + rules: + - linters: + - dupl + - errcheck + - gocyclo + - gosec + path: _test\.go + - linters: + - forbidigo + path-except: _test\.go + - linters: + - lll + source: "^//go:generate " + paths: + - third_party$ + - builtin$ + - examples$ +issues: + max-issues-per-linter: 0 + max-same-issues: 0 +severity: + default: error + rules: + - linters: + - dupl + severity: info +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/api/.vscode/launch.json b/api/.vscode/launch.json new file mode 100644 index 0000000..55263cc --- /dev/null +++ b/api/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Server", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceRoot}/server.go", + "cwd": "${workspaceRoot}", + "env": { + "PATH": "${env:PATH}:/Applications/darktable.app/Contents/MacOS/" + }, + "args": [] + } + ] +} diff --git a/api/database/database.go b/api/database/database.go new file mode 100644 index 0000000..823b08f --- /dev/null +++ b/api/database/database.go @@ -0,0 +1,220 @@ +package database + +import ( + "context" + "errors" + "fmt" + "log" + "net/url" + "time" + + "github.com/photoview/photoview/api/database/drivers" + "github.com/photoview/photoview/api/database/migrations" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/utils" + + "github.com/go-sql-driver/mysql" + gorm_mysql "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func GetMysqlAddress(addressString string) (string, error) { + if addressString == "" { + return "", fmt.Errorf("Environment variable %s missing, exiting", utils.EnvMysqlURL.GetName()) + } + + config, err := mysql.ParseDSN(addressString) + if err != nil { + return "", fmt.Errorf("could not parse mysql url: %w", err) + } + + config.MultiStatements = true + config.ParseTime = true + + return config.FormatDSN(), nil +} + +func GetPostgresAddress(addressString string) (*url.URL, error) { + if addressString == "" { + return nil, fmt.Errorf("Environment variable %s missing, exiting", utils.EnvPostgresURL.GetName()) + } + + address, err := url.Parse(addressString) + if err != nil { + return nil, fmt.Errorf("could not parse postgres url: %w", err) + } + + return address, nil +} + +func GetSqliteAddress(path string) (*url.URL, error) { + if path == "" { + path = "photoview.db" + } + + address, err := url.Parse(path) + if err != nil { + return nil, fmt.Errorf("could not parse sqlite url (%s): %w", path, err) + } + + queryValues := address.Query() + queryValues.Add("cache", "shared") + queryValues.Add("mode", "rwc") + // queryValues.Add("_busy_timeout", "60000") // 1 minute + queryValues.Add("_journal_mode", "WAL") // Write-Ahead Logging (WAL) mode + queryValues.Add("_locking_mode", "NORMAL") // allows concurrent reads and writes + queryValues.Add("_foreign_keys", "ON") // Enforc foreign key constraints. + address.RawQuery = queryValues.Encode() + + // log.Panicf("%s", address.String()) + + return address, nil +} + +func ConfigureDatabase(config *gorm.Config) (*gorm.DB, error) { + var databaseDialect gorm.Dialector + driver := drivers.DatabaseDriverFromEnv() + log.Printf("Utilizing %s database driver based on environment variables", driver) + + switch driver { + case drivers.MYSQL: + mysqlAddress, err := GetMysqlAddress(utils.EnvMysqlURL.GetValue()) + if err != nil { + return nil, err + } + databaseDialect = gorm_mysql.Open(mysqlAddress) + case drivers.SQLITE: + sqliteAddress, err := GetSqliteAddress(utils.EnvSqlitePath.GetValue()) + if err != nil { + return nil, err + } + databaseDialect = sqlite.Open(sqliteAddress.String()) + + case drivers.POSTGRES: + postgresAddress, err := GetPostgresAddress(utils.EnvPostgresURL.GetValue()) + if err != nil { + return nil, err + } + databaseDialect = postgres.Open(postgresAddress.String()) + } + + db, err := gorm.Open(databaseDialect, config) + if err != nil { + return nil, err + } + + return db, nil +} + +// SetupDatabase connects to the database using environment variables +func SetupDatabase() (*gorm.DB, error) { + + config := gorm.Config{} + + // Configure database logging + if utils.DevelopmentMode() { + config.Logger = logger.Default.LogMode(logger.Info) + } else { + config.Logger = logger.Default.LogMode(logger.Warn) + } + + var db *gorm.DB + + for retryCount := 1; retryCount <= 5; retryCount++ { + + var err error + db, err = ConfigureDatabase(&config) + if err == nil { + sqlDB, dbErr := db.DB() + if dbErr != nil { + return nil, dbErr + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + err = sqlDB.PingContext(ctx) + cancel() + + sqlDB.SetMaxOpenConns(80) + + if err == nil { + return db, nil + } + } + + log.Printf("WARN: Could not ping database: %s. Will retry after 5 seconds\n", err) + time.Sleep(time.Duration(5) * time.Second) + } + + return db, nil +} + +var database_models []interface{} = []interface{}{ + &models.User{}, + &models.AccessToken{}, + &models.SiteInfo{}, + &models.Media{}, + &models.MediaURL{}, + &models.Album{}, + &models.MediaEXIF{}, + &models.VideoMetadata{}, + &models.ShareToken{}, + &models.UserMediaData{}, + &models.UserAlbums{}, + &models.UserPreferences{}, + + // Face detection + &models.FaceGroup{}, + &models.ImageFace{}, +} + +func MigrateDatabase(db *gorm.DB) error { + + if err := db.SetupJoinTable(&models.User{}, "Albums", &models.UserAlbums{}); err != nil { + log.Printf("Setup UserAlbums join table failed: %v\n", err) + } + + if err := db.AutoMigrate(database_models...); err != nil { + log.Printf("Auto migration failed: %v\n", err) + } + + // v2.1.0 - Replaced by Media.CreatedAt + if db.Migrator().HasColumn(&models.Media{}, "date_imported") { + db.Migrator().DropColumn(&models.Media{}, "date_imported") + } + + // v2.3.0 - Changed type of MediaEXIF.Exposure and MediaEXIF.Flash + // from string values to decimal and int respectively + if err := migrateExifFields(db); err != nil { + log.Printf("Failed to run exif fields migration: %v\n", err) + } + + // Remove invalid GPS data from DB + if err := migrations.MigrateForExifGPSCorrection(db); err != nil { + log.Printf("Failed to run exif GPS correction migration: %v\n", err) + } + + // v2.5.0 - Remove Thumbnail Method for Downsampliing filters + if db.Migrator().HasColumn(&models.SiteInfo{}, "thumbnail_method") { + db.Migrator().DropColumn(&models.SiteInfo{}, "thumbnail_method") + } + + return nil +} + +func ClearDatabase(db *gorm.DB) error { + var errs []error + for _, model := range database_models { + if err := db.Migrator().DropTable(model); err != nil { + errs = append(errs, err) + } + } + + if err := errors.Join(errs...); err != nil { + return fmt.Errorf("drop tables error: %w", err) + } + + return nil +} diff --git a/api/database/drivers/database_drivers.go b/api/database/drivers/database_drivers.go new file mode 100644 index 0000000..a88ee0d --- /dev/null +++ b/api/database/drivers/database_drivers.go @@ -0,0 +1,55 @@ +package drivers + +import ( + "strings" + + "github.com/photoview/photoview/api/utils" + "gorm.io/gorm" +) + +// DatabaseDriverType represents the name of a database driver +type DatabaseDriverType string + +const ( + MYSQL DatabaseDriverType = "mysql" + SQLITE DatabaseDriverType = "sqlite" + POSTGRES DatabaseDriverType = "postgres" +) + +func DatabaseDriverFromEnv() DatabaseDriverType { + + var driver DatabaseDriverType + driverString := strings.ToLower(utils.EnvDatabaseDriver.GetValue()) + + switch driverString { + case "mysql": + driver = MYSQL + case "sqlite": + driver = SQLITE + case "postgres": + driver = POSTGRES + default: + driver = MYSQL + } + + return driver +} + +func (driver DatabaseDriverType) MatchDatabase(db *gorm.DB) bool { + return db.Dialector.Name() == string(driver) +} + +func GetDatabaseDriverType(db *gorm.DB) (driver DatabaseDriverType) { + switch db.Dialector.Name() { + case "mysql": + driver = MYSQL + case "sqlite": + driver = SQLITE + case "postgres": + driver = POSTGRES + default: + driver = MYSQL + } + + return +} diff --git a/api/database/helpers.go b/api/database/helpers.go new file mode 100644 index 0000000..a9e194b --- /dev/null +++ b/api/database/helpers.go @@ -0,0 +1,46 @@ +package database + +import ( + "fmt" + "log" + + "github.com/photoview/photoview/api/database/drivers" + "gorm.io/gorm" +) + +// DateComponent a component of a date (day, month, year) +type DateComponent string + +const ( + DateCompYear DateComponent = "YEAR" + DateCompMonth DateComponent = "MONTH" + DateCompDay DateComponent = "DAY" +) + +// DateExtract is a helper function that is used to generate the proper SQL syntax +// for extracting date components (day, month, year) for different database backends. +func DateExtract(db *gorm.DB, component DateComponent, attribute string) string { + + var result string + + switch drivers.GetDatabaseDriverType(db) { + case drivers.MYSQL, drivers.POSTGRES: + result = fmt.Sprintf("EXTRACT(%s FROM %s)", component, attribute) + case drivers.SQLITE: + var sqliteFormatted string + switch component { + case DateCompYear: + sqliteFormatted = "%Y" + case DateCompMonth: + sqliteFormatted = "%m" + case DateCompDay: + sqliteFormatted = "%d" + } + + result = fmt.Sprintf("CAST(strftime('%s', %s) AS INTEGER)", sqliteFormatted, attribute) + default: + log.Panicf("unsupported database backend: %s", drivers.GetDatabaseDriverType(db)) + } + + return result +} diff --git a/api/database/migration_exif.go b/api/database/migration_exif.go new file mode 100644 index 0000000..73effbd --- /dev/null +++ b/api/database/migration_exif.go @@ -0,0 +1,214 @@ +package database + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +type exifModel struct { + ID int `gorm:"primarykey"` + Exposure *string + Flash *string +} + +var flashDescriptions = map[int]string{ + 0x0: "No Flash", + 0x1: "Fired", + 0x5: "Fired, Return not detected", + 0x7: "Fired, Return detected", + 0x8: "On, Did not fire", + 0x9: "On, Fired", + 0xD: "On, Return not detected", + 0xF: "On, Return detected", + 0x10: "Off, Did not fire", + 0x14: "Off, Did not fire, Return not detected", + 0x18: "Auto, Did not fire", + 0x19: "Auto, Fired", + 0x1D: "Auto, Fired, Return not detected", + 0x1F: "Auto, Fired, Return detected", + 0x20: "No flash function", + 0x30: "Off, No flash function", + 0x41: "Fired, Red-eye reduction", + 0x45: "Fired, Red-eye reduction, Return not detected", + 0x47: "Fired, Red-eye reduction, Return detected", + 0x49: "On, Red-eye reduction", + 0x4D: "On, Red-eye reduction, Return not detected", + 0x4F: "On, Red-eye reduction, Return detected", + 0x50: "Off, Red-eye reduction", + 0x58: "Auto, Did not fire, Red-eye reduction", + 0x59: "Auto, Fired, Red-eye reduction", + 0x5D: "Auto, Fired, Red-eye reduction, Return not detected", + 0x5F: "Auto, Fired, Red-eye reduction, Return detected", +} + +// Migrate MediaExif fields "exposure" and "flash" from strings to integers +func migrateExifFields(db *gorm.DB) error { + mediaExifColumns, err := db.Migrator().ColumnTypes(&models.MediaEXIF{}) + if err != nil { + return err + } + + return db.Transaction(func(tx *gorm.DB) error { + for _, exifCol := range mediaExifColumns { + if err := parseExposure(exifCol, db); err != nil { + return err + } + + if err := parseFlash(exifCol, db); err != nil { + return err + } + } + + if err := db.AutoMigrate(&models.MediaEXIF{}); err != nil { + return fmt.Errorf("failed to auto migrate media_exif after exposure conversion: %w", err) + } + + return nil + }) +} + +func parseFlash(exifCol gorm.ColumnType, db *gorm.DB) error { + if exifCol.Name() == "flash" { + switch exifCol.DatabaseTypeName() { + case "double", "numeric", "real", "bigint", "integer": + // correct type, do nothing + default: + // do migration + if err := migrateExifFieldsFlash(db); err != nil { + return err + } + } + } + return nil +} + +func parseExposure(exifCol gorm.ColumnType, db *gorm.DB) error { + if exifCol.Name() == "exposure" { + switch exifCol.DatabaseTypeName() { + case "double", "numeric", "real", "bigint", "integer": + // correct type, do nothing + default: + // do migration + if err := migrateExifFieldsExposure(db); err != nil { + return err + } + } + } + return nil +} + +func migrateExifFieldsExposure(db *gorm.DB) error { + log.Println("Migrating `media_exif.exposure` from string to double") + + err := db.Transaction(func(tx *gorm.DB) error { + + if err := tx.Exec("UPDATE media_exif SET exposure = NULL WHERE exposure = ''").Error; err != nil { + return fmt.Errorf("convert flash attribute empty values to NULL: %w", err) + } + + var results []exifModel + + return calculateExposure(tx, results) + }) + + if err != nil { + return fmt.Errorf("migrating `media_exif.exposure` failed: %w", err) + } + + return nil +} + +func calculateExposure(tx *gorm.DB, results []exifModel) error { + return tx.Model(&exifModel{}).Table("media_exif").Where("exposure LIKE '%/%'").FindInBatches( + &results, 100, func(tx *gorm.DB, batch int) error { + for _, result := range results { + + if result.Exposure == nil { + continue + } + + frac := strings.Split(*result.Exposure, "/") + if len(frac) != 2 { + return fmt.Errorf("failed to convert exposure value (%s) expected format x/y", frac) + } + + numerator, err := strconv.ParseFloat(frac[0], 64) + if err != nil { + return err + } + + denominator, err := strconv.ParseFloat(frac[1], 64) + if err != nil { + return err + } + + decimalValue := numerator / denominator + *result.Exposure = fmt.Sprintf("%f", decimalValue) + } + + tx.Save(&results) + + return nil + }).Error +} + +func migrateExifFieldsFlash(db *gorm.DB) error { + log.Println("Migrating `media_exif.flash` from string to int") + + err := db.Transaction(func(tx *gorm.DB) error { + + var dataType string + if err := tx.Raw( + "SELECT data_type FROM information_schema.columns WHERE table_name = 'media_exif' AND column_name = 'flash';"). + Find(&dataType).Error; err != nil { + + return fmt.Errorf("read data_type of column media_exif.flash: %w", err) + } + + if dataType == "bigint" { + return nil + } + + if err := tx.Exec("UPDATE media_exif SET flash = NULL WHERE flash = ''").Error; err != nil { + return fmt.Errorf("convert flash attribute empty values to NULL: %w", err) + } + + var results []exifModel + + return replaceFlashValues(tx, results) + }) + + if err != nil { + return fmt.Errorf("migrating `media_exif.flash` failed: %w", err) + } + + return nil +} + +func replaceFlashValues(tx *gorm.DB, results []exifModel) error { + return tx.Model(&exifModel{}).Table("media_exif").Where("flash IS NOT NULL").FindInBatches( + &results, 100, func(tx *gorm.DB, batch int) error { + for _, result := range results { + + if result.Flash == nil { + continue + } + + for index, name := range flashDescriptions { + if *result.Flash == name { + *result.Flash = fmt.Sprintf("%d", index) + break + } + } + } + + tx.Save(&results) + + return nil + }).Error +} diff --git a/api/database/migrations/exif_invalid_gps.go b/api/database/migrations/exif_invalid_gps.go new file mode 100644 index 0000000..9d0a637 --- /dev/null +++ b/api/database/migrations/exif_invalid_gps.go @@ -0,0 +1,24 @@ +package migrations + +import ( + "fmt" + + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +// MigrateForExifGPSCorrection finds and removes invalid GPS data from media_exif table +func MigrateForExifGPSCorrection(db *gorm.DB) error { + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&models.MediaEXIF{}). + Where("ABS(gps_longitude) > ?", 90). + Or("ABS(gps_latitude) > ?", 90). + Updates(map[string]interface{}{ + "gps_latitude": nil, + "gps_longitude": nil, + }).Error; err != nil { + return fmt.Errorf("failed to remove invalid GPS data from media_exif table: %w", err) + } + return nil + }) +} diff --git a/api/database/migrations/exif_invalid_gps_test.go b/api/database/migrations/exif_invalid_gps_test.go new file mode 100644 index 0000000..94cbd18 --- /dev/null +++ b/api/database/migrations/exif_invalid_gps_test.go @@ -0,0 +1,54 @@ +package migrations_test + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/photoview/photoview/api/database/migrations" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" +) + +func TestExifMigration(t *testing.T) { + db := test_utils.DatabaseTest(t) + defer db.Exec("DELETE FROM media_exif") // Clean up after test + + // Create test data + exifEntries := []models.MediaEXIF{ + {GPSLatitude: floatPtr(90.1), GPSLongitude: floatPtr(90.0)}, // Invalid GPSLatitude + {GPSLatitude: floatPtr(-90.1), GPSLongitude: floatPtr(-90.0)}, // Invalid GPSLatitude + {GPSLatitude: floatPtr(90.0), GPSLongitude: floatPtr(90.1)}, // Invalid GPSLongitude + {GPSLatitude: floatPtr(-90.0), GPSLongitude: floatPtr(-90.1)}, // Invalid GPSLongitude + {GPSLatitude: floatPtr(90.0), GPSLongitude: floatPtr(90.0)}, // Valid GPS data + {GPSLatitude: floatPtr(-90.0), GPSLongitude: floatPtr(-90.0)}, // Valid GPS data + {GPSLatitude: floatPtr(90.1), GPSLongitude: floatPtr(90.1)}, // Invalid GPSLatitude and GPSLongitude + {GPSLatitude: floatPtr(-90.1), GPSLongitude: floatPtr(-90.1)}, // Invalid GPSLatitude and GPSLongitude + } + + // Insert test data + for _, entry := range exifEntries { + assert.NoError(t, db.Create(&entry).Error) + } + + // Run migration + assert.NoError(t, migrations.MigrateForExifGPSCorrection(db)) + + // Validate the results + var results []models.MediaEXIF + assert.NoError(t, db.Find(&results).Error) + + for _, entry := range results { + if entry.GPSLatitude != nil { + assert.LessOrEqual(t, math.Abs(*entry.GPSLatitude), 90.0, "GPSLatitude should be within [-90, 90]: %+v", entry) + } + if entry.GPSLongitude != nil { + assert.LessOrEqual(t, math.Abs(*entry.GPSLongitude), 90.0, "GPSLongitude should be within [-90, 90]: %+v", entry) + } + } +} + +func floatPtr(f float64) *float64 { + return &f +} diff --git a/api/database/migrations/migrations_test.go b/api/database/migrations/migrations_test.go new file mode 100644 index 0000000..6371f02 --- /dev/null +++ b/api/database/migrations/migrations_test.go @@ -0,0 +1,11 @@ +package migrations_test + +import ( + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} diff --git a/api/dataloader/gen_mediaurlloader.go b/api/dataloader/gen_mediaurlloader.go new file mode 100644 index 0000000..99c9d92 --- /dev/null +++ b/api/dataloader/gen_mediaurlloader.go @@ -0,0 +1,224 @@ +// Code generated by github.com/vektah/dataloaden, DO NOT EDIT. + +package dataloader + +import ( + "sync" + "time" + + "github.com/photoview/photoview/api/graphql/models" +) + +// MediaURLLoaderConfig captures the config to create a new MediaURLLoader +type MediaURLLoaderConfig struct { + // Fetch is a method that provides the data for the loader + Fetch func(keys []int) ([]*models.MediaURL, []error) + + // Wait is how long wait before sending a batch + Wait time.Duration + + // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit + MaxBatch int +} + +// NewMediaURLLoader creates a new MediaURLLoader given a fetch, wait, and maxBatch +func NewMediaURLLoader(config MediaURLLoaderConfig) *MediaURLLoader { + return &MediaURLLoader{ + fetch: config.Fetch, + wait: config.Wait, + maxBatch: config.MaxBatch, + } +} + +// MediaURLLoader batches and caches requests +type MediaURLLoader struct { + // this method provides the data for the loader + fetch func(keys []int) ([]*models.MediaURL, []error) + + // how long to done before sending a batch + wait time.Duration + + // this will limit the maximum number of keys to send in one batch, 0 = no limit + maxBatch int + + // INTERNAL + + // lazily created cache + cache map[int]*models.MediaURL + + // the current batch. keys will continue to be collected until timeout is hit, + // then everything will be sent to the fetch method and out to the listeners + batch *mediaURLLoaderBatch + + // mutex to prevent races + mu sync.Mutex +} + +type mediaURLLoaderBatch struct { + keys []int + data []*models.MediaURL + error []error + closing bool + done chan struct{} +} + +// Load a MediaURL by key, batching and caching will be applied automatically +func (l *MediaURLLoader) Load(key int) (*models.MediaURL, error) { + return l.LoadThunk(key)() +} + +// LoadThunk returns a function that when called will block waiting for a MediaURL. +// This method should be used if you want one goroutine to make requests to many +// different data loaders without blocking until the thunk is called. +func (l *MediaURLLoader) LoadThunk(key int) func() (*models.MediaURL, error) { + l.mu.Lock() + if it, ok := l.cache[key]; ok { + l.mu.Unlock() + return func() (*models.MediaURL, error) { + return it, nil + } + } + if l.batch == nil { + l.batch = &mediaURLLoaderBatch{done: make(chan struct{})} + } + batch := l.batch + pos := batch.keyIndex(l, key) + l.mu.Unlock() + + return func() (*models.MediaURL, error) { + <-batch.done + + var data *models.MediaURL + if pos < len(batch.data) { + data = batch.data[pos] + } + + var err error + // its convenient to be able to return a single error for everything + if len(batch.error) == 1 { + err = batch.error[0] + } else if batch.error != nil { + err = batch.error[pos] + } + + if err == nil { + l.mu.Lock() + l.unsafeSet(key, data) + l.mu.Unlock() + } + + return data, err + } +} + +// LoadAll fetches many keys at once. It will be broken into appropriate sized +// sub batches depending on how the loader is configured +func (l *MediaURLLoader) LoadAll(keys []int) ([]*models.MediaURL, []error) { + results := make([]func() (*models.MediaURL, error), len(keys)) + + for i, key := range keys { + results[i] = l.LoadThunk(key) + } + + mediaURLs := make([]*models.MediaURL, len(keys)) + errors := make([]error, len(keys)) + for i, thunk := range results { + mediaURLs[i], errors[i] = thunk() + } + return mediaURLs, errors +} + +// LoadAllThunk returns a function that when called will block waiting for a MediaURLs. +// This method should be used if you want one goroutine to make requests to many +// different data loaders without blocking until the thunk is called. +func (l *MediaURLLoader) LoadAllThunk(keys []int) func() ([]*models.MediaURL, []error) { + results := make([]func() (*models.MediaURL, error), len(keys)) + for i, key := range keys { + results[i] = l.LoadThunk(key) + } + return func() ([]*models.MediaURL, []error) { + mediaURLs := make([]*models.MediaURL, len(keys)) + errors := make([]error, len(keys)) + for i, thunk := range results { + mediaURLs[i], errors[i] = thunk() + } + return mediaURLs, errors + } +} + +// Prime the cache with the provided key and value. If the key already exists, no change is made +// and false is returned. +// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) +func (l *MediaURLLoader) Prime(key int, value *models.MediaURL) bool { + l.mu.Lock() + var found bool + if _, found = l.cache[key]; !found { + // make a copy when writing to the cache, its easy to pass a pointer in from a loop var + // and end up with the whole cache pointing to the same value. + cpy := *value + l.unsafeSet(key, &cpy) + } + l.mu.Unlock() + return !found +} + +// Clear the value at key from the cache, if it exists +func (l *MediaURLLoader) Clear(key int) { + l.mu.Lock() + delete(l.cache, key) + l.mu.Unlock() +} + +func (l *MediaURLLoader) unsafeSet(key int, value *models.MediaURL) { + if l.cache == nil { + l.cache = map[int]*models.MediaURL{} + } + l.cache[key] = value +} + +// keyIndex will return the location of the key in the batch, if its not found +// it will add the key to the batch +func (b *mediaURLLoaderBatch) keyIndex(l *MediaURLLoader, key int) int { + for i, existingKey := range b.keys { + if key == existingKey { + return i + } + } + + pos := len(b.keys) + b.keys = append(b.keys, key) + if pos == 0 { + go b.startTimer(l) + } + + if l.maxBatch != 0 && pos >= l.maxBatch-1 { + if !b.closing { + b.closing = true + l.batch = nil + go b.end(l) + } + } + + return pos +} + +func (b *mediaURLLoaderBatch) startTimer(l *MediaURLLoader) { + time.Sleep(l.wait) + l.mu.Lock() + + // we must have hit a batch limit and are already finalizing this batch + if b.closing { + l.mu.Unlock() + return + } + + l.batch = nil + l.mu.Unlock() + + b.end(l) +} + +func (b *mediaURLLoaderBatch) end(l *MediaURLLoader) { + b.data, b.error = l.fetch(b.keys) + close(b.done) +} diff --git a/api/dataloader/gen_userfavoritesloader.go b/api/dataloader/gen_userfavoritesloader.go new file mode 100644 index 0000000..6ab333c --- /dev/null +++ b/api/dataloader/gen_userfavoritesloader.go @@ -0,0 +1,221 @@ +// Code generated by github.com/vektah/dataloaden, DO NOT EDIT. + +package dataloader + +import ( + "sync" + "time" + + "github.com/photoview/photoview/api/graphql/models" +) + +// UserFavoritesLoaderConfig captures the config to create a new UserFavoritesLoader +type UserFavoritesLoaderConfig struct { + // Fetch is a method that provides the data for the loader + Fetch func(keys []*models.UserMediaData) ([]bool, []error) + + // Wait is how long wait before sending a batch + Wait time.Duration + + // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit + MaxBatch int +} + +// NewUserFavoritesLoader creates a new UserFavoritesLoader given a fetch, wait, and maxBatch +func NewUserFavoritesLoader(config UserFavoritesLoaderConfig) *UserFavoritesLoader { + return &UserFavoritesLoader{ + fetch: config.Fetch, + wait: config.Wait, + maxBatch: config.MaxBatch, + } +} + +// UserFavoritesLoader batches and caches requests +type UserFavoritesLoader struct { + // this method provides the data for the loader + fetch func(keys []*models.UserMediaData) ([]bool, []error) + + // how long to done before sending a batch + wait time.Duration + + // this will limit the maximum number of keys to send in one batch, 0 = no limit + maxBatch int + + // INTERNAL + + // lazily created cache + cache map[*models.UserMediaData]bool + + // the current batch. keys will continue to be collected until timeout is hit, + // then everything will be sent to the fetch method and out to the listeners + batch *userFavoritesLoaderBatch + + // mutex to prevent races + mu sync.Mutex +} + +type userFavoritesLoaderBatch struct { + keys []*models.UserMediaData + data []bool + error []error + closing bool + done chan struct{} +} + +// Load a bool by key, batching and caching will be applied automatically +func (l *UserFavoritesLoader) Load(key *models.UserMediaData) (bool, error) { + return l.LoadThunk(key)() +} + +// LoadThunk returns a function that when called will block waiting for a bool. +// This method should be used if you want one goroutine to make requests to many +// different data loaders without blocking until the thunk is called. +func (l *UserFavoritesLoader) LoadThunk(key *models.UserMediaData) func() (bool, error) { + l.mu.Lock() + if it, ok := l.cache[key]; ok { + l.mu.Unlock() + return func() (bool, error) { + return it, nil + } + } + if l.batch == nil { + l.batch = &userFavoritesLoaderBatch{done: make(chan struct{})} + } + batch := l.batch + pos := batch.keyIndex(l, key) + l.mu.Unlock() + + return func() (bool, error) { + <-batch.done + + var data bool + if pos < len(batch.data) { + data = batch.data[pos] + } + + var err error + // its convenient to be able to return a single error for everything + if len(batch.error) == 1 { + err = batch.error[0] + } else if batch.error != nil { + err = batch.error[pos] + } + + if err == nil { + l.mu.Lock() + l.unsafeSet(key, data) + l.mu.Unlock() + } + + return data, err + } +} + +// LoadAll fetches many keys at once. It will be broken into appropriate sized +// sub batches depending on how the loader is configured +func (l *UserFavoritesLoader) LoadAll(keys []*models.UserMediaData) ([]bool, []error) { + results := make([]func() (bool, error), len(keys)) + + for i, key := range keys { + results[i] = l.LoadThunk(key) + } + + bools := make([]bool, len(keys)) + errors := make([]error, len(keys)) + for i, thunk := range results { + bools[i], errors[i] = thunk() + } + return bools, errors +} + +// LoadAllThunk returns a function that when called will block waiting for a bools. +// This method should be used if you want one goroutine to make requests to many +// different data loaders without blocking until the thunk is called. +func (l *UserFavoritesLoader) LoadAllThunk(keys []*models.UserMediaData) func() ([]bool, []error) { + results := make([]func() (bool, error), len(keys)) + for i, key := range keys { + results[i] = l.LoadThunk(key) + } + return func() ([]bool, []error) { + bools := make([]bool, len(keys)) + errors := make([]error, len(keys)) + for i, thunk := range results { + bools[i], errors[i] = thunk() + } + return bools, errors + } +} + +// Prime the cache with the provided key and value. If the key already exists, no change is made +// and false is returned. +// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) +func (l *UserFavoritesLoader) Prime(key *models.UserMediaData, value bool) bool { + l.mu.Lock() + var found bool + if _, found = l.cache[key]; !found { + l.unsafeSet(key, value) + } + l.mu.Unlock() + return !found +} + +// Clear the value at key from the cache, if it exists +func (l *UserFavoritesLoader) Clear(key *models.UserMediaData) { + l.mu.Lock() + delete(l.cache, key) + l.mu.Unlock() +} + +func (l *UserFavoritesLoader) unsafeSet(key *models.UserMediaData, value bool) { + if l.cache == nil { + l.cache = map[*models.UserMediaData]bool{} + } + l.cache[key] = value +} + +// keyIndex will return the location of the key in the batch, if its not found +// it will add the key to the batch +func (b *userFavoritesLoaderBatch) keyIndex(l *UserFavoritesLoader, key *models.UserMediaData) int { + for i, existingKey := range b.keys { + if key == existingKey { + return i + } + } + + pos := len(b.keys) + b.keys = append(b.keys, key) + if pos == 0 { + go b.startTimer(l) + } + + if l.maxBatch != 0 && pos >= l.maxBatch-1 { + if !b.closing { + b.closing = true + l.batch = nil + go b.end(l) + } + } + + return pos +} + +func (b *userFavoritesLoaderBatch) startTimer(l *UserFavoritesLoader) { + time.Sleep(l.wait) + l.mu.Lock() + + // we must have hit a batch limit and are already finalizing this batch + if b.closing { + l.mu.Unlock() + return + } + + l.batch = nil + l.mu.Unlock() + + b.end(l) +} + +func (b *userFavoritesLoaderBatch) end(l *UserFavoritesLoader) { + b.data, b.error = l.fetch(b.keys) + close(b.done) +} diff --git a/api/dataloader/gen_userloader.go b/api/dataloader/gen_userloader.go new file mode 100644 index 0000000..ec4d53f --- /dev/null +++ b/api/dataloader/gen_userloader.go @@ -0,0 +1,224 @@ +// Code generated by github.com/vektah/dataloaden, DO NOT EDIT. + +package dataloader + +import ( + "sync" + "time" + + "github.com/photoview/photoview/api/graphql/models" +) + +// UserLoaderConfig captures the config to create a new UserLoader +type UserLoaderConfig struct { + // Fetch is a method that provides the data for the loader + Fetch func(keys []string) ([]*models.User, []error) + + // Wait is how long wait before sending a batch + Wait time.Duration + + // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit + MaxBatch int +} + +// NewUserLoader creates a new UserLoader given a fetch, wait, and maxBatch +func NewUserLoader(config UserLoaderConfig) *UserLoader { + return &UserLoader{ + fetch: config.Fetch, + wait: config.Wait, + maxBatch: config.MaxBatch, + } +} + +// UserLoader batches and caches requests +type UserLoader struct { + // this method provides the data for the loader + fetch func(keys []string) ([]*models.User, []error) + + // how long to done before sending a batch + wait time.Duration + + // this will limit the maximum number of keys to send in one batch, 0 = no limit + maxBatch int + + // INTERNAL + + // lazily created cache + cache map[string]*models.User + + // the current batch. keys will continue to be collected until timeout is hit, + // then everything will be sent to the fetch method and out to the listeners + batch *userLoaderBatch + + // mutex to prevent races + mu sync.Mutex +} + +type userLoaderBatch struct { + keys []string + data []*models.User + error []error + closing bool + done chan struct{} +} + +// Load a User by key, batching and caching will be applied automatically +func (l *UserLoader) Load(key string) (*models.User, error) { + return l.LoadThunk(key)() +} + +// LoadThunk returns a function that when called will block waiting for a User. +// This method should be used if you want one goroutine to make requests to many +// different data loaders without blocking until the thunk is called. +func (l *UserLoader) LoadThunk(key string) func() (*models.User, error) { + l.mu.Lock() + if it, ok := l.cache[key]; ok { + l.mu.Unlock() + return func() (*models.User, error) { + return it, nil + } + } + if l.batch == nil { + l.batch = &userLoaderBatch{done: make(chan struct{})} + } + batch := l.batch + pos := batch.keyIndex(l, key) + l.mu.Unlock() + + return func() (*models.User, error) { + <-batch.done + + var data *models.User + if pos < len(batch.data) { + data = batch.data[pos] + } + + var err error + // its convenient to be able to return a single error for everything + if len(batch.error) == 1 { + err = batch.error[0] + } else if batch.error != nil { + err = batch.error[pos] + } + + if err == nil { + l.mu.Lock() + l.unsafeSet(key, data) + l.mu.Unlock() + } + + return data, err + } +} + +// LoadAll fetches many keys at once. It will be broken into appropriate sized +// sub batches depending on how the loader is configured +func (l *UserLoader) LoadAll(keys []string) ([]*models.User, []error) { + results := make([]func() (*models.User, error), len(keys)) + + for i, key := range keys { + results[i] = l.LoadThunk(key) + } + + users := make([]*models.User, len(keys)) + errors := make([]error, len(keys)) + for i, thunk := range results { + users[i], errors[i] = thunk() + } + return users, errors +} + +// LoadAllThunk returns a function that when called will block waiting for a Users. +// This method should be used if you want one goroutine to make requests to many +// different data loaders without blocking until the thunk is called. +func (l *UserLoader) LoadAllThunk(keys []string) func() ([]*models.User, []error) { + results := make([]func() (*models.User, error), len(keys)) + for i, key := range keys { + results[i] = l.LoadThunk(key) + } + return func() ([]*models.User, []error) { + users := make([]*models.User, len(keys)) + errors := make([]error, len(keys)) + for i, thunk := range results { + users[i], errors[i] = thunk() + } + return users, errors + } +} + +// Prime the cache with the provided key and value. If the key already exists, no change is made +// and false is returned. +// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) +func (l *UserLoader) Prime(key string, value *models.User) bool { + l.mu.Lock() + var found bool + if _, found = l.cache[key]; !found { + // make a copy when writing to the cache, its easy to pass a pointer in from a loop var + // and end up with the whole cache pointing to the same value. + cpy := *value + l.unsafeSet(key, &cpy) + } + l.mu.Unlock() + return !found +} + +// Clear the value at key from the cache, if it exists +func (l *UserLoader) Clear(key string) { + l.mu.Lock() + delete(l.cache, key) + l.mu.Unlock() +} + +func (l *UserLoader) unsafeSet(key string, value *models.User) { + if l.cache == nil { + l.cache = map[string]*models.User{} + } + l.cache[key] = value +} + +// keyIndex will return the location of the key in the batch, if its not found +// it will add the key to the batch +func (b *userLoaderBatch) keyIndex(l *UserLoader, key string) int { + for i, existingKey := range b.keys { + if key == existingKey { + return i + } + } + + pos := len(b.keys) + b.keys = append(b.keys, key) + if pos == 0 { + go b.startTimer(l) + } + + if l.maxBatch != 0 && pos >= l.maxBatch-1 { + if !b.closing { + b.closing = true + l.batch = nil + go b.end(l) + } + } + + return pos +} + +func (b *userLoaderBatch) startTimer(l *UserLoader) { + time.Sleep(l.wait) + l.mu.Lock() + + // we must have hit a batch limit and are already finalizing this batch + if b.closing { + l.mu.Unlock() + return + } + + l.batch = nil + l.mu.Unlock() + + b.end(l) +} + +func (b *userLoaderBatch) end(l *UserLoader) { + b.data, b.error = l.fetch(b.keys) + close(b.done) +} diff --git a/api/dataloader/loaders.go b/api/dataloader/loaders.go new file mode 100644 index 0000000..dd19e75 --- /dev/null +++ b/api/dataloader/loaders.go @@ -0,0 +1,48 @@ +package dataloader + +import ( + "context" + "net/http" + + "github.com/gorilla/mux" + "gorm.io/gorm" +) + +type contextKey string + +const loadersKey contextKey = "dataloaders" + +type Loaders struct { + MediaThumbnail *MediaURLLoader + MediaHighres *MediaURLLoader + MediaVideoWeb *MediaURLLoader + UserFromAccessToken *UserLoader + UserMediaFavorite *UserFavoritesLoader +} + +func Middleware(db *gorm.DB) mux.MiddlewareFunc { + return mux.MiddlewareFunc(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + ctx := context.WithValue(r.Context(), loadersKey, &Loaders{ + MediaThumbnail: NewThumbnailMediaURLLoader(db), + MediaHighres: NewHighresMediaURLLoader(db), + MediaVideoWeb: NewVideoWebMediaURLLoader(db), + UserFromAccessToken: NewUserLoaderByToken(db), + UserMediaFavorite: NewUserFavoriteLoader(db), + }) + + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + }) + }) +} + +func For(ctx context.Context) *Loaders { + loaders, ok := ctx.Value(loadersKey).(*Loaders) + if !ok { + return nil + } + return loaders +} diff --git a/api/dataloader/mediaURLLoader.go b/api/dataloader/mediaURLLoader.go new file mode 100644 index 0000000..a9f9eec --- /dev/null +++ b/api/dataloader/mediaURLLoader.go @@ -0,0 +1,82 @@ +package dataloader + +import ( + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_type" + "github.com/pkg/errors" + + "gorm.io/gorm" +) + +func makeMediaURLLoader(db *gorm.DB, filter func(query *gorm.DB) *gorm.DB) func(keys []int) ([]*models.MediaURL, []error) { + return func(mediaIDs []int) ([]*models.MediaURL, []error) { + + var urls []*models.MediaURL + query := db.Where("media_id IN (?)", mediaIDs) + + query = filter(query) + + if err := query.Find(&urls).Error; err != nil { + return nil, []error{errors.Wrap(err, "media url loader database query")} + } + + resultMap := make(map[int]*models.MediaURL, len(mediaIDs)) + for _, url := range urls { + resultMap[url.MediaID] = url + } + + result := make([]*models.MediaURL, len(mediaIDs)) + for i, mediaID := range mediaIDs { + mediaURL, found := resultMap[mediaID] + if found { + result[i] = mediaURL + } else { + result[i] = nil + } + } + + return result, nil + } +} + +func NewThumbnailMediaURLLoader(db *gorm.DB) *MediaURLLoader { + return &MediaURLLoader{ + maxBatch: 100, + wait: 5 * time.Millisecond, + fetch: makeMediaURLLoader(db, func(query *gorm.DB) *gorm.DB { + return query.Where("purpose IN ?", []string{string(models.PhotoThumbnail), string(models.VideoThumbnail)}) + }), + } +} + +func NewHighresMediaURLLoader(db *gorm.DB) *MediaURLLoader { + return &MediaURLLoader{ + maxBatch: 100, + wait: 5 * time.Millisecond, + fetch: makeMediaURLLoader(db, func(query *gorm.DB) *gorm.DB { + return query. + Where("(purpose = ? OR (purpose = ? AND content_type IN ?))", models.PhotoHighRes, models.MediaOriginal, media_type.WebMimetypes). + //PhotoHighRes consistently wins ordering when both exist, which is preferred for web delivery + Order("media_id ASC, CASE purpose WHEN '" + + string(models.MediaOriginal) + "' THEN 0 WHEN '" + + string(models.PhotoHighRes) + "' THEN 1 END ASC") + }), + } +} + +func NewVideoWebMediaURLLoader(db *gorm.DB) *MediaURLLoader { + return &MediaURLLoader{ + maxBatch: 100, + wait: 5 * time.Millisecond, + fetch: makeMediaURLLoader(db, func(query *gorm.DB) *gorm.DB { + return query. + Where("purpose IN ?", []string{string(models.VideoWeb), string(models.MediaOriginal)}). + //VideoWeb consistently wins ordering when both exist, which is preferred for web delivery + Order("media_id ASC, CASE purpose WHEN '" + + string(models.MediaOriginal) + "' THEN 0 WHEN '" + + string(models.VideoWeb) + "' THEN 1 END ASC") + }), + } +} diff --git a/api/dataloader/userFavoriteLoader.go b/api/dataloader/userFavoriteLoader.go new file mode 100644 index 0000000..46eaa5c --- /dev/null +++ b/api/dataloader/userFavoriteLoader.go @@ -0,0 +1,60 @@ +package dataloader + +import ( + "time" + + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +func NewUserFavoriteLoader(db *gorm.DB) *UserFavoritesLoader { + return &UserFavoritesLoader{ + maxBatch: 100, + wait: 5 * time.Millisecond, + fetch: func(keys []*models.UserMediaData) ([]bool, []error) { + + userIDMap := make(map[int]struct{}, len(keys)) + mediaIDMap := make(map[int]struct{}, len(keys)) + for _, key := range keys { + userIDMap[key.UserID] = struct{}{} + mediaIDMap[key.MediaID] = struct{}{} + } + + + uniqueUserIDs := make([]int, len(userIDMap)) + uniqueMediaIDs := make([]int, len(mediaIDMap)) + + count := 0 + for id := range userIDMap { + uniqueUserIDs[count] = id + count++ + } + + count = 0 + for id := range mediaIDMap { + uniqueMediaIDs[count] = id + count++ + } + + var userMediaFavorites []*models.UserMediaData + err := db.Where("user_id IN (?)", uniqueUserIDs).Where("media_id IN (?)", uniqueMediaIDs).Where("favorite = TRUE").Find(&userMediaFavorites).Error + if err != nil { + return nil, []error{err} + } + + result := make([]bool, len(keys)) + for i, key := range keys { + favorite := false + for _, fav := range userMediaFavorites { + if fav.UserID == key.UserID && fav.MediaID == key.MediaID { + favorite = true + break + } + } + result[i] = favorite + } + + return result, nil + }, + } +} diff --git a/api/dataloader/userLoader.go b/api/dataloader/userLoader.go new file mode 100644 index 0000000..64ac016 --- /dev/null +++ b/api/dataloader/userLoader.go @@ -0,0 +1,71 @@ +package dataloader + +import ( + "time" + + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +func NewUserLoaderByToken(db *gorm.DB) *UserLoader { + return &UserLoader{ + maxBatch: 100, + wait: 5 * time.Millisecond, + fetch: func(tokens []string) ([]*models.User, []error) { + + var accessTokens []*models.AccessToken + err := db.Where("expire > ?", time.Now()).Where("value IN (?)", tokens).Find(&accessTokens).Error + if err != nil { + return nil, []error{err} + } + + rows, err := db.Table("access_tokens").Select("distinct user_id").Where("expire > ?", time.Now()).Where("value IN (?)", tokens).Rows() + if err != nil { + return nil, []error{err} + } + userIDs := make([]int, 0) + for rows.Next() { + var id int + if err := db.ScanRows(rows, &id); err != nil { + return nil, []error{err} + } + userIDs = append(userIDs, id) + } + rows.Close() + + var userMap map[int]*models.User + if len(userIDs) > 0 { + + var users []*models.User + if err := db.Where("id IN (?)", userIDs).Find(&users).Error; err != nil { + return nil, []error{err} + } + + userMap = make(map[int]*models.User, len(users)) + for _, user := range users { + userMap[user.ID] = user + } + } else { + userMap = make(map[int]*models.User, 0) + } + + tokenMap := make(map[string]*models.AccessToken, len(tokens)) + for _, token := range accessTokens { + tokenMap[token.Value] = token + } + + result := make([]*models.User, len(tokens)) + for i, token := range tokens { + accessToken, tokenFound := tokenMap[token] + if tokenFound { + user, userFound := userMap[accessToken.UserID] + if userFound { + result[i] = user + } + } + } + + return result, nil + }, + } +} diff --git a/api/example.env b/api/example.env new file mode 100644 index 0000000..df6ff10 --- /dev/null +++ b/api/example.env @@ -0,0 +1,35 @@ +# Copy this file to .env + +PHOTOVIEW_DATABASE_DRIVER=sqlite + +# Specifies the filepath for the sqlite database, if PHOTOVIEW_DATABASE_DRIVER is set to 'sqlite' +PHOTOVIEW_SQLITE_PATH=photoview.db + +# Specifies the connection string for the mysql database, if PHOTOVIEW_DATABASE_DRIVER is set to 'mysql' +# PHOTOVIEW_MYSQL_URL=user:password@tcp(localhost)/dbname + +# Specifies the connection string for the postgres database, if PHOTOVIEW_DATABASE_DRIVER is set to 'postgres' +# See https://www.postgresql.org/docs/current/libpq-ssl.html for possible ssl modes +# PHOTOVIEW_POSTGRES_URL=postgres://user:password@host:port/dbname?sslmode=(disable|allow|...) + +PHOTOVIEW_LISTEN_IP=0.0.0.0 +PHOTOVIEW_LISTEN_PORT=4001 + +# Path where media should be cached, defaults to ./media_cache +# PHOTOVIEW_MEDIA_CACHE=./media_cache + +# Set to 1 for the server to also serve the built static ui files +PHOTOVIEW_SERVE_UI=0 + +# Enter a valid mapbox token, to enable maps feature +# A token can be created for free at https://mapbox.com +#MAPBOX_TOKEN= + +# Set to 1 to set server in development mode, this enables graphql playground +# Remove this if running in production +PHOTOVIEW_DEVELOPMENT_MODE=1 + +# Set the hardware acceleration when encoding videos. +# Support `qsv`, `vaapi`, `nvenc`. +# Only `qsv` is verified with `/dev/dri//dev/dri` devices. +# PHOTOVIEW_VIDEO_HARDWARE_ACCELERATION= diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..b6fc501 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,61 @@ +module github.com/photoview/photoview/api + +go 1.26.0 + +require ( + github.com/99designs/gqlgen v0.17.90 + github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e + github.com/buckket/go-blurhash v1.1.0 + github.com/go-sql-driver/mysql v1.10.0 + github.com/google/go-cmp v0.7.0 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 + github.com/otiai10/copy v1.14.1 + github.com/pkg/errors v0.9.1 + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 + github.com/stretchr/testify v1.11.1 + github.com/vektah/gqlparser/v2 v2.5.33 + github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 + golang.org/x/crypto v0.51.0 + golang.org/x/text v0.37.0 + gopkg.in/gographics/imagick.v3 v3.7.3 + gopkg.in/vansante/go-ffprobe.v2 v2.3.0 + gorm.io/driver/mysql v1.6.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/kr/text v0.1.0 // indirect + github.com/mattn/go-sqlite3 v1.14.23 // indirect + github.com/otiai10/mint v1.6.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sosodev/duration v1.4.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/urfave/cli/v3 v3.8.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/tools v0.44.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +tool github.com/99designs/gqlgen diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..21b5f62 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,126 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/99designs/gqlgen v0.17.90 h1:wSv6blm/PoplU6QoNw83EcQpNtC0HX3/+44vITJOzpk= +github.com/99designs/gqlgen v0.17.90/go.mod h1:GqYrEwYsqCG8VaOsq2kJUCUKwAE1T+u2i+Nj7NtXiVI= +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc= +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU= +github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= +github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do= +github.com/buckket/go-blurhash v1.1.0/go.mod h1:aT2iqo5W9vu9GpyoLErKfTHwgODsZp3bQfXjXJUxNb8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= +github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/vektah/gqlparser/v2 v2.5.33 h1:lRp8aIeNUNbimf/axZd7ETg24q06hBtPaas+TcvI/7E= +github.com/vektah/gqlparser/v2 v2.5.33/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= +github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/gographics/imagick.v3 v3.7.3 h1:Hy2MbJKLJ/9T3ZuV1zwBOy09O9prf2MCCVpM7bcZdpY= +gopkg.in/gographics/imagick.v3 v3.7.3/go.mod h1:7I4S9VWdwr88yzYi7g+ZL4H8oZuH9cmSQI7GsZCcYFM= +gopkg.in/vansante/go-ffprobe.v2 v2.3.0 h1:YhEzASq5eN8m73j/WqhRbqzJrx5gaqRueHZ9ZC53o/o= +gopkg.in/vansante/go-ffprobe.v2 v2.3.0/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/api/gqlgen.yml b/api/gqlgen.yml new file mode 100644 index 0000000..ed194e6 --- /dev/null +++ b/api/gqlgen.yml @@ -0,0 +1,72 @@ +# Refer to https://gqlgen.com/config/ + +schema: + - graphql/resolvers/*.graphql + +exec: + filename: graphql/generated.go + +model: + filename: graphql/models/generated.go + package: models + +resolver: + layout: follow-schema + dir: graphql/resolvers + package: resolvers + filename_template: "{name}.go" + +autobind: [] + +models: + ID: + model: github.com/99designs/gqlgen/graphql.IntID + User: + model: github.com/photoview/photoview/api/graphql/models.User + fields: + albums: + resolver: true + UserPreferences: + model: github.com/photoview/photoview/api/graphql/models.UserPreferences + Media: + model: github.com/photoview/photoview/api/graphql/models.Media + fields: + exif: + resolver: true + faces: + resolver: true + type: + resolver: true + album: + resolver: true + MediaURL: + model: github.com/photoview/photoview/api/graphql/models.MediaURL + MediaEXIF: + model: github.com/photoview/photoview/api/graphql/models.MediaEXIF + fields: + dateShot: + fieldName: DateShotWithOffset + VideoMetadata: + model: github.com/photoview/photoview/api/graphql/models.VideoMetadata + Album: + model: github.com/photoview/photoview/api/graphql/models.Album + ShareToken: + model: github.com/photoview/photoview/api/graphql/models.ShareToken + FaceGroup: + model: github.com/photoview/photoview/api/graphql/models.FaceGroup + fields: + imageFaces: + resolver: true + ImageFace: + model: github.com/photoview/photoview/api/graphql/models.ImageFace + fields: + faceGroup: + resolver: true + media: + resolver: true + FaceRectangle: + model: github.com/photoview/photoview/api/graphql/models.FaceRectangle + SiteInfo: + model: github.com/photoview/photoview/api/graphql/models.SiteInfo + MediaType: + model: github.com/photoview/photoview/api/graphql/models.MediaType diff --git a/api/graphql/auth/auth.go b/api/graphql/auth/auth.go new file mode 100644 index 0000000..4764a51 --- /dev/null +++ b/api/graphql/auth/auth.go @@ -0,0 +1,131 @@ +package auth + +import ( + "context" + "errors" + "net/http" + "regexp" + + "github.com/99designs/gqlgen/graphql/handler/transport" + "github.com/photoview/photoview/api/dataloader" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "gorm.io/gorm" +) + +var ErrUnauthorized = errors.New("unauthorized") +var bearerRegex = regexp.MustCompile("^(?i)Bearer ([a-zA-Z0-9]{24})$") + +const INVALID_AUTH_TOKEN = "invalid authorization token" +const INTERNAL_SERVER_ERROR = "internal server error" + +// A private key for context that only this package can access. This is important +// to prevent collisions between different context uses +var userCtxKey = &contextKey{"user"} + +type contextKey struct { + name string +} + +// Middleware decodes the share session cookie and packs the session into context +func Middleware(db *gorm.DB) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + if tokenCookie, err := r.Cookie("auth-token"); err == nil { + loaders := dataloader.For(r.Context()) + if loaders == nil { + log.Error(r.Context(), "Dataloader not available in HTTP context") + http.Error(w, INTERNAL_SERVER_ERROR, http.StatusInternalServerError) + return + } + + user, err := loaders.UserFromAccessToken.Load(tokenCookie.Value) + // Check for dataloader errors (database failures, etc.) + if err != nil { + log.Error(r.Context(), "Error loading user from token", "error", err) + http.Error(w, INVALID_AUTH_TOKEN, http.StatusUnauthorized) + return + } + + // If user is nil, the token doesn't exist or is invalid + if user == nil { + log.Error(r.Context(), "Token not found in database") + http.Error(w, INVALID_AUTH_TOKEN, http.StatusUnauthorized) + return + } + + // put it in context + ctx := AddUserToContext(r.Context(), user) + + // and call the next with our new context + r = r.WithContext(ctx) + } else { + log.Info(r.Context(), "Did not find auth-token cookie") + } + + next.ServeHTTP(w, r) + }) + } +} + +func AddUserToContext(ctx context.Context, user *models.User) context.Context { + return context.WithValue(ctx, userCtxKey, user) +} + +func TokenFromBearer(bearer *string) (*string, error) { + matches := bearerRegex.FindStringSubmatch(*bearer) + if len(matches) != 2 { + return nil, errors.New("invalid bearer format") + } + + token := matches[1] + return &token, nil +} + +// UserFromContext finds the user from the context. REQUIRES Middleware to have run. +func UserFromContext(ctx context.Context) *models.User { + raw, _ := ctx.Value(userCtxKey).(*models.User) + return raw +} + +func AuthWebsocketInit() func(context.Context, transport.InitPayload) (context.Context, *transport.InitPayload, error) { + return func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) { + + bearer, exists := initPayload["Authorization"].(string) + if !exists { + return ctx, nil, nil + } + + token, err := TokenFromBearer(&bearer) + if err != nil { + log.Error(ctx, "Invalid bearer format (websocket)", "error", err) + return nil, nil, err + } + + loaders := dataloader.For(ctx) + if loaders == nil { + log.Error(ctx, "Dataloader not available in websocket context") + return nil, nil, errors.New(INTERNAL_SERVER_ERROR) + } + + user, err := loaders.UserFromAccessToken.Load(*token) + if err != nil { + log.Error(ctx, "Error loading user from token (websocket)", "error", err) + return nil, nil, errors.New(INVALID_AUTH_TOKEN) + } + + // Check if token exists in database + if user == nil { + log.Error(ctx, "Token not found in database (websocket)") + return nil, nil, errors.New(INVALID_AUTH_TOKEN) + } + + // put it in context + userCtx := context.WithValue(ctx, userCtxKey, user) + + // and return it so the resolvers can see it + // Return nil for the InitPayload acknowledgment (no custom ack payload needed) + return userCtx, nil, nil + } +} diff --git a/api/graphql/auth/auth_test.go b/api/graphql/auth/auth_test.go new file mode 100644 index 0000000..703c8fa --- /dev/null +++ b/api/graphql/auth/auth_test.go @@ -0,0 +1,244 @@ +package auth_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/99designs/gqlgen/graphql/handler/transport" + "github.com/photoview/photoview/api/dataloader" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + test_utils.UnitTestRun(m) +} + +func TestTokenFromBearer(t *testing.T) { + + testsValues := []struct { + name string + bearer string + out string + valid bool + }{ + {"Valid bearer", "Bearer ZY9YfxFa3TapSAD37XUBFryo", "ZY9YfxFa3TapSAD37XUBFryo", true}, + {"Case insensitive bearer", "bEaReR ZY9YfxFa3TapSAD37XUBFryo", "ZY9YfxFa3TapSAD37XUBFryo", true}, + {"Missing bearer start", "ZY9YfxFa3TapSAD37XUBFryo", "", false}, + {"Empty input", "", "", false}, + {"Invalid token value", "Bearer THIS_IS_INVALID", "", false}, + } + + for _, test := range testsValues { + t.Run(test.name, func(t *testing.T) { + token, err := auth.TokenFromBearer(&test.bearer) + if test.valid { + assert.NoError(t, err) + assert.NotNil(t, token) + assert.Equal(t, test.out, *token) + } else { + assert.Error(t, err) + assert.Nil(t, token) + } + }) + } +} + +func TestAuthWebsocketInit(t *testing.T) { + db := test_utils.DatabaseTest(t) + + // Create test user and token + password := "test_password" + user, err := models.RegisterUser(db, "testuser", &password, false) + assert.NoError(t, err) + + token, err := user.GenerateAccessToken(db) + assert.NoError(t, err) + + testCases := []struct { + name string + initPayload transport.InitPayload + expectError bool + expectUser bool + expectNilCtx bool + }{ + { + name: "Valid authorization", + initPayload: transport.InitPayload{"Authorization": "Bearer " + token.Value}, + expectError: false, + expectUser: true, + }, + { + name: "Missing authorization", + initPayload: transport.InitPayload{}, + expectError: false, + expectUser: false, + }, + { + name: "Invalid bearer format", + initPayload: transport.InitPayload{"Authorization": "InvalidFormat"}, + expectError: true, + expectNilCtx: true, + }, + { + name: "Invalid token", + initPayload: transport.InitPayload{"Authorization": "Bearer INVALID_TOKEN_123456"}, + expectError: true, + expectNilCtx: true, + }, + { + name: "Empty token", + initPayload: transport.InitPayload{"Authorization": "Bearer "}, + expectError: true, + expectNilCtx: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + initFunc := auth.AuthWebsocketInit() + ctx := context.Background() + req := httptest.NewRequest("GET", "/", nil) + req = req.WithContext(ctx) + + var contextWithLoaders context.Context + handler := dataloader.Middleware(db)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + contextWithLoaders = r.Context() + })) + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + resultCtx, ackPayload, err := initFunc(contextWithLoaders, tc.initPayload) + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if tc.expectNilCtx { + assert.Nil(t, resultCtx) + } else { + assert.NotNil(t, resultCtx) + } + + // Verify InitPayload acknowledgment is always nil (as per PR implementation) + assert.Nil(t, ackPayload) + + if tc.expectUser { + retrievedUser := auth.UserFromContext(resultCtx) + assert.NotNil(t, retrievedUser) + assert.Equal(t, user.ID, retrievedUser.ID) + assert.Equal(t, "testuser", retrievedUser.Username) + } else if !tc.expectNilCtx { + retrievedUser := auth.UserFromContext(resultCtx) + assert.Nil(t, retrievedUser) + } + }) + } +} + +func TestMiddleware(t *testing.T) { + db := test_utils.DatabaseTest(t) + + // Create test user and token + password := "test_password" + user, err := models.RegisterUser(db, "testuser", &password, false) + assert.NoError(t, err) + + token, err := user.GenerateAccessToken(db) + assert.NoError(t, err) + + testCases := []struct { + name string + cookieValue string + setCookie bool + expectStatus int + expectUser bool + }{ + { + name: "Valid token cookie", + cookieValue: token.Value, + setCookie: true, + expectStatus: 200, + expectUser: true, + }, + { + name: "No cookie", + setCookie: false, + expectStatus: 200, + expectUser: false, + }, + { + name: "Invalid token", + cookieValue: "INVALID_TOKEN", + setCookie: true, + expectStatus: 401, + expectUser: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/graphql", nil) + if tc.setCookie { + req.AddCookie(&http.Cookie{ + Name: "auth-token", + Value: tc.cookieValue, + }) + } + + var capturedContext context.Context + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedContext = r.Context() + }) + + authHandler := auth.Middleware(db)(handler) + fullHandler := dataloader.Middleware(db)(authHandler) + + recorder := httptest.NewRecorder() + fullHandler.ServeHTTP(recorder, req) + + assert.Equal(t, tc.expectStatus, recorder.Code) + + if tc.expectUser { + retrievedUser := auth.UserFromContext(capturedContext) + assert.NotNil(t, retrievedUser) + assert.Equal(t, user.ID, retrievedUser.ID) + } else if recorder.Code == 200 { + // Handler was called, verify no user in context + retrievedUser := auth.UserFromContext(capturedContext) + assert.Nil(t, retrievedUser) + } + }) + } +} + +func TestContextUserOperations(t *testing.T) { + db := test_utils.DatabaseTest(t) + + password := "test" + user, err := models.RegisterUser(db, "testuser", &password, false) + assert.NoError(t, err) + + t.Run("AddUserToContext and UserFromContext", func(t *testing.T) { + ctx := context.Background() + + // Initially no user + retrieved := auth.UserFromContext(ctx) + assert.Nil(t, retrieved) + + // Add user + ctxWithUser := auth.AddUserToContext(ctx, user) + + // Retrieve user + retrieved = auth.UserFromContext(ctxWithUser) + assert.NotNil(t, retrieved) + assert.Equal(t, user.ID, retrieved.ID) + assert.Equal(t, "testuser", retrieved.Username) + }) +} diff --git a/api/graphql/directive.go b/api/graphql/directive.go new file mode 100644 index 0000000..a8126cc --- /dev/null +++ b/api/graphql/directive.go @@ -0,0 +1,27 @@ +package api + +import ( + "context" + "errors" + + "github.com/99designs/gqlgen/graphql" + "github.com/photoview/photoview/api/graphql/auth" +) + +func IsAdmin(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { + user := auth.UserFromContext(ctx) + if user == nil || user.Admin == false { + return nil, errors.New("user must be admin") + } + + return next(ctx) +} + +func IsAuthorized(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return next(ctx) +} diff --git a/api/graphql/endpoint/graphql_endpoint.go b/api/graphql/endpoint/graphql_endpoint.go new file mode 100644 index 0000000..1852ae7 --- /dev/null +++ b/api/graphql/endpoint/graphql_endpoint.go @@ -0,0 +1,52 @@ +package graphql_endpoint + +import ( + "time" + + graphql_handler "github.com/99designs/gqlgen/graphql/handler" + "github.com/99designs/gqlgen/graphql/handler/extension" + "github.com/99designs/gqlgen/graphql/handler/lru" + "github.com/99designs/gqlgen/graphql/handler/transport" + photoview_graphql "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/resolvers" + "github.com/photoview/photoview/api/server" + "github.com/photoview/photoview/api/utils" + "github.com/vektah/gqlparser/v2/ast" + "gorm.io/gorm" +) + +func GraphqlEndpoint(db *gorm.DB) *graphql_handler.Server { + graphqlResolver := resolvers.NewRootResolver(db) + graphqlDirective := photoview_graphql.DirectiveRoot{} + graphqlDirective.IsAdmin = photoview_graphql.IsAdmin + graphqlDirective.IsAuthorized = photoview_graphql.IsAuthorized + + graphqlConfig := photoview_graphql.Config{ + Resolvers: &graphqlResolver, + Directives: graphqlDirective, + } + + graphqlServer := graphql_handler.New(photoview_graphql.NewExecutableSchema(graphqlConfig)) + graphqlServer.AddTransport(transport.Websocket{ + KeepAlivePingInterval: 10 * time.Second, + Upgrader: server.WebsocketUpgrader(utils.DevelopmentMode()), + InitFunc: auth.AuthWebsocketInit(), + }) + graphqlServer.AddTransport(transport.Options{}) + graphqlServer.AddTransport(transport.GET{}) + graphqlServer.AddTransport(transport.POST{}) + graphqlServer.AddTransport(transport.MultipartForm{}) + + graphqlServer.SetQueryCache(lru.New[*ast.QueryDocument](1000)) + + graphqlServer.Use(extension.AutomaticPersistedQuery{ + Cache: lru.New[string](100), + }) + + if utils.DevelopmentMode() { + graphqlServer.Use(extension.Introspection{}) + } + + return graphqlServer +} diff --git a/api/graphql/endpoint/graphql_endpoint_test.go b/api/graphql/endpoint/graphql_endpoint_test.go new file mode 100644 index 0000000..75b5eaa --- /dev/null +++ b/api/graphql/endpoint/graphql_endpoint_test.go @@ -0,0 +1,22 @@ +package graphql_endpoint_test + +import ( + "testing" + + graphql_endpoint "github.com/photoview/photoview/api/graphql/endpoint" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + test_utils.UnitTestRun(m) +} + +func TestGraphqlEndpoint(t *testing.T) { + db := test_utils.DatabaseTest(t) + + t.Run("creates server successfully", func(t *testing.T) { + server := graphql_endpoint.GraphqlEndpoint(db) + assert.NotNil(t, server) + }) +} diff --git a/api/graphql/generated.go b/api/graphql/generated.go new file mode 100644 index 0000000..8e6d234 --- /dev/null +++ b/api/graphql/generated.go @@ -0,0 +1,13101 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package api + +import ( + "bytes" + "context" + "embed" + "errors" + "fmt" + "math" + "strconv" + "sync/atomic" + "time" + + "github.com/99designs/gqlgen/graphql" + "github.com/99designs/gqlgen/graphql/introspection" + "github.com/photoview/photoview/api/graphql/models" + gqlparser "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" +) + +// region ************************** generated!.gotpl ************************** + +// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. +func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { + return &executableSchema{SchemaData: cfg.Schema, Resolvers: cfg.Resolvers, Directives: cfg.Directives, ComplexityRoot: cfg.Complexity} +} + +type Config = graphql.Config[ResolverRoot, DirectiveRoot, ComplexityRoot] + +type ResolverRoot interface { + Album() AlbumResolver + FaceGroup() FaceGroupResolver + ImageFace() ImageFaceResolver + Media() MediaResolver + Mutation() MutationResolver + Query() QueryResolver + ShareToken() ShareTokenResolver + SiteInfo() SiteInfoResolver + Subscription() SubscriptionResolver + User() UserResolver +} + +type DirectiveRoot struct { + IsAdmin func(ctx context.Context, obj any, next graphql.Resolver) (res any, err error) + IsAuthorized func(ctx context.Context, obj any, next graphql.Resolver) (res any, err error) +} + +type ComplexityRoot struct { + Album struct { + FilePath func(childComplexity int) int + ID func(childComplexity int) int + Media func(childComplexity int, order *models.Ordering, paginate *models.Pagination, onlyFavorites *bool) int + Owner func(childComplexity int) int + ParentAlbum func(childComplexity int) int + Path func(childComplexity int) int + Shares func(childComplexity int) int + SubAlbums func(childComplexity int, order *models.Ordering, paginate *models.Pagination) int + Thumbnail func(childComplexity int) int + Title func(childComplexity int) int + } + + AuthorizeResult struct { + Status func(childComplexity int) int + Success func(childComplexity int) int + Token func(childComplexity int) int + } + + Coordinates struct { + Latitude func(childComplexity int) int + Longitude func(childComplexity int) int + } + + FaceGroup struct { + ID func(childComplexity int) int + ImageFaceCount func(childComplexity int) int + ImageFaces func(childComplexity int, paginate *models.Pagination) int + Label func(childComplexity int) int + } + + FaceRectangle struct { + MaxX func(childComplexity int) int + MaxY func(childComplexity int) int + MinX func(childComplexity int) int + MinY func(childComplexity int) int + } + + ImageFace struct { + FaceGroup func(childComplexity int) int + ID func(childComplexity int) int + Media func(childComplexity int) int + Rectangle func(childComplexity int) int + } + + Media struct { + Album func(childComplexity int) int + Blurhash func(childComplexity int) int + Date func(childComplexity int) int + Downloads func(childComplexity int) int + Exif func(childComplexity int) int + Faces func(childComplexity int) int + Favorite func(childComplexity int) int + HighRes func(childComplexity int) int + ID func(childComplexity int) int + Path func(childComplexity int) int + Shares func(childComplexity int) int + Thumbnail func(childComplexity int) int + Title func(childComplexity int) int + Type func(childComplexity int) int + VideoMetadata func(childComplexity int) int + VideoWeb func(childComplexity int) int + } + + MediaDownload struct { + MediaURL func(childComplexity int) int + Title func(childComplexity int) int + } + + MediaEXIF struct { + Aperture func(childComplexity int) int + Camera func(childComplexity int) int + Coordinates func(childComplexity int) int + DateShotWithOffset func(childComplexity int) int + Description func(childComplexity int) int + Exposure func(childComplexity int) int + ExposureProgram func(childComplexity int) int + Flash func(childComplexity int) int + FocalLength func(childComplexity int) int + ID func(childComplexity int) int + Iso func(childComplexity int) int + Lens func(childComplexity int) int + Maker func(childComplexity int) int + Media func(childComplexity int) int + } + + MediaURL struct { + FileSize func(childComplexity int) int + Height func(childComplexity int) int + URL func(childComplexity int) int + Width func(childComplexity int) int + } + + Mutation struct { + AuthorizeUser func(childComplexity int, username string, password string) int + ChangeUserPreferences func(childComplexity int, language *string) int + CombineFaceGroups func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupIDs []int) int + CreateUser func(childComplexity int, username string, password *string, admin bool) int + DeleteShareToken func(childComplexity int, token string) int + DeleteUser func(childComplexity int, id int) int + DetachImageFaces func(childComplexity int, imageFaceIDs []int) int + FavoriteMedia func(childComplexity int, mediaID int, favorite bool) int + InitialSetupWizard func(childComplexity int, username string, password string, rootPath string) int + MoveImageFaces func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int + ProtectShareToken func(childComplexity int, token string, password *string) int + RecognizeUnlabeledFaces func(childComplexity int) int + ResetAlbumCover func(childComplexity int, albumID int) int + ScanAll func(childComplexity int) int + ScanUser func(childComplexity int, userID int) int + SetAlbumCover func(childComplexity int, coverID int) int + SetExpireShareToken func(childComplexity int, token string, expire *time.Time) int + SetFaceGroupLabel func(childComplexity int, faceGroupID int, label *string) int + SetPeriodicScanInterval func(childComplexity int, interval int) int + SetScannerConcurrentWorkers func(childComplexity int, workers int) int + ShareAlbum func(childComplexity int, albumID int, expire *time.Time, password *string) int + ShareMedia func(childComplexity int, mediaID int, expire *time.Time, password *string) int + UpdateUser func(childComplexity int, id int, username *string, password *string, admin *bool) int + UserAddRootPath func(childComplexity int, id int, rootPath string) int + UserRemoveRootAlbum func(childComplexity int, userID int, albumID int) int + } + + Notification struct { + Content func(childComplexity int) int + Header func(childComplexity int) int + Key func(childComplexity int) int + Negative func(childComplexity int) int + Positive func(childComplexity int) int + Progress func(childComplexity int) int + Timeout func(childComplexity int) int + Type func(childComplexity int) int + } + + Query struct { + Album func(childComplexity int, id int, tokenCredentials *models.ShareTokenCredentials) int + FaceGroup func(childComplexity int, id int) int + MapboxToken func(childComplexity int) int + Media func(childComplexity int, id int, tokenCredentials *models.ShareTokenCredentials) int + MediaList func(childComplexity int, ids []int) int + MyAlbums func(childComplexity int, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int + MyFaceGroups func(childComplexity int, paginate *models.Pagination) int + MyMedia func(childComplexity int, order *models.Ordering, paginate *models.Pagination) int + MyMediaGeoJSON func(childComplexity int) int + MyTimeline func(childComplexity int, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) int + MyUser func(childComplexity int) int + MyUserPreferences func(childComplexity int) int + Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int + ShareToken func(childComplexity int, credentials models.ShareTokenCredentials) int + ShareTokenValidatePassword func(childComplexity int, credentials models.ShareTokenCredentials) int + SiteInfo func(childComplexity int) int + User func(childComplexity int, order *models.Ordering, paginate *models.Pagination) int + } + + ScannerResult struct { + Finished func(childComplexity int) int + Message func(childComplexity int) int + Progress func(childComplexity int) int + Success func(childComplexity int) int + } + + SearchResult struct { + Albums func(childComplexity int) int + Media func(childComplexity int) int + Query func(childComplexity int) int + } + + ShareToken struct { + Album func(childComplexity int) int + Expire func(childComplexity int) int + HasPassword func(childComplexity int) int + ID func(childComplexity int) int + Media func(childComplexity int) int + Owner func(childComplexity int) int + Token func(childComplexity int) int + } + + SiteInfo struct { + ConcurrentWorkers func(childComplexity int) int + FaceDetectionEnabled func(childComplexity int) int + InitialSetup func(childComplexity int) int + PeriodicScanInterval func(childComplexity int) int + } + + Subscription struct { + Notification func(childComplexity int) int + } + + TimelineGroup struct { + Album func(childComplexity int) int + Date func(childComplexity int) int + Media func(childComplexity int) int + MediaTotal func(childComplexity int) int + } + + User struct { + Admin func(childComplexity int) int + Albums func(childComplexity int) int + ID func(childComplexity int) int + RootAlbums func(childComplexity int) int + Username func(childComplexity int) int + } + + UserPreferences struct { + ID func(childComplexity int) int + Language func(childComplexity int) int + } + + VideoMetadata struct { + Audio func(childComplexity int) int + Bitrate func(childComplexity int) int + Codec func(childComplexity int) int + ColorProfile func(childComplexity int) int + Duration func(childComplexity int) int + Framerate func(childComplexity int) int + Height func(childComplexity int) int + ID func(childComplexity int) int + Media func(childComplexity int) int + Width func(childComplexity int) int + } +} + +type AlbumResolver interface { + Media(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination, onlyFavorites *bool) ([]*models.Media, error) + SubAlbums(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination) ([]*models.Album, error) + + Owner(ctx context.Context, obj *models.Album) (*models.User, error) + + Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error) + Path(ctx context.Context, obj *models.Album) ([]*models.Album, error) + Shares(ctx context.Context, obj *models.Album) ([]*models.ShareToken, error) +} +type FaceGroupResolver interface { + ImageFaces(ctx context.Context, obj *models.FaceGroup, paginate *models.Pagination) ([]*models.ImageFace, error) + ImageFaceCount(ctx context.Context, obj *models.FaceGroup) (int, error) +} +type ImageFaceResolver interface { + Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error) + + FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error) +} +type MediaResolver interface { + Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error) + HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error) + VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error) + Album(ctx context.Context, obj *models.Media) (*models.Album, error) + Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, error) + + Favorite(ctx context.Context, obj *models.Media) (bool, error) + Type(ctx context.Context, obj *models.Media) (models.MediaType, error) + + Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error) + Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error) + Faces(ctx context.Context, obj *models.Media) ([]*models.ImageFace, error) +} +type MutationResolver interface { + ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error) + SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error) + SetFaceGroupLabel(ctx context.Context, faceGroupID int, label *string) (*models.FaceGroup, error) + CombineFaceGroups(ctx context.Context, destinationFaceGroupID int, sourceFaceGroupIDs []int) (*models.FaceGroup, error) + MoveImageFaces(ctx context.Context, imageFaceIDs []int, destinationFaceGroupID int) (*models.FaceGroup, error) + RecognizeUnlabeledFaces(ctx context.Context) ([]*models.ImageFace, error) + DetachImageFaces(ctx context.Context, imageFaceIDs []int) (*models.FaceGroup, error) + FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) + ScanAll(ctx context.Context) (*models.ScannerResult, error) + ScanUser(ctx context.Context, userID int) (*models.ScannerResult, error) + SetPeriodicScanInterval(ctx context.Context, interval int) (int, error) + SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error) + ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) + ShareMedia(ctx context.Context, mediaID int, expire *time.Time, password *string) (*models.ShareToken, error) + DeleteShareToken(ctx context.Context, token string) (*models.ShareToken, error) + ProtectShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) + SetExpireShareToken(ctx context.Context, token string, expire *time.Time) (*models.ShareToken, error) + AuthorizeUser(ctx context.Context, username string, password string) (*models.AuthorizeResult, error) + InitialSetupWizard(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) + UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) + CreateUser(ctx context.Context, username string, password *string, admin bool) (*models.User, error) + DeleteUser(ctx context.Context, id int) (*models.User, error) + UserAddRootPath(ctx context.Context, id int, rootPath string) (*models.Album, error) + UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error) + ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) +} +type QueryResolver interface { + MyAlbums(ctx context.Context, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) + Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) + MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) + FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error) + MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) + Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) + MediaList(ctx context.Context, ids []int) ([]*models.Media, error) + MyMediaGeoJSON(ctx context.Context) (any, error) + MapboxToken(ctx context.Context) (*string, error) + Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error) + ShareToken(ctx context.Context, credentials models.ShareTokenCredentials) (*models.ShareToken, error) + ShareTokenValidatePassword(ctx context.Context, credentials models.ShareTokenCredentials) (bool, error) + SiteInfo(ctx context.Context) (*models.SiteInfo, error) + MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) + User(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.User, error) + MyUser(ctx context.Context) (*models.User, error) + MyUserPreferences(ctx context.Context) (*models.UserPreferences, error) +} +type ShareTokenResolver interface { + HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error) +} +type SiteInfoResolver interface { + FaceDetectionEnabled(ctx context.Context, obj *models.SiteInfo) (bool, error) +} +type SubscriptionResolver interface { + Notification(ctx context.Context) (<-chan *models.Notification, error) +} +type UserResolver interface { + Albums(ctx context.Context, obj *models.User) ([]*models.Album, error) + RootAlbums(ctx context.Context, obj *models.User) ([]*models.Album, error) +} + +type executableSchema graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot] + +func (e *executableSchema) Schema() *ast.Schema { + if e.SchemaData != nil { + return e.SchemaData + } + return parsedSchema +} + +func (e *executableSchema) Complexity(ctx context.Context, typeName, field string, childComplexity int, rawArgs map[string]any) (int, bool) { + ec := newExecutionContext(nil, e, nil) + _ = ec + switch typeName + "." + field { + + case "Album.filePath": + if e.ComplexityRoot.Album.FilePath == nil { + break + } + + return e.ComplexityRoot.Album.FilePath(childComplexity), true + case "Album.id": + if e.ComplexityRoot.Album.ID == nil { + break + } + + return e.ComplexityRoot.Album.ID(childComplexity), true + case "Album.media": + if e.ComplexityRoot.Album.Media == nil { + break + } + + args, err := ec.field_Album_media_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Album.Media(childComplexity, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool)), true + case "Album.owner": + if e.ComplexityRoot.Album.Owner == nil { + break + } + + return e.ComplexityRoot.Album.Owner(childComplexity), true + case "Album.parentAlbum": + if e.ComplexityRoot.Album.ParentAlbum == nil { + break + } + + return e.ComplexityRoot.Album.ParentAlbum(childComplexity), true + case "Album.path": + if e.ComplexityRoot.Album.Path == nil { + break + } + + return e.ComplexityRoot.Album.Path(childComplexity), true + case "Album.shares": + if e.ComplexityRoot.Album.Shares == nil { + break + } + + return e.ComplexityRoot.Album.Shares(childComplexity), true + case "Album.subAlbums": + if e.ComplexityRoot.Album.SubAlbums == nil { + break + } + + args, err := ec.field_Album_subAlbums_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Album.SubAlbums(childComplexity, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination)), true + case "Album.thumbnail": + if e.ComplexityRoot.Album.Thumbnail == nil { + break + } + + return e.ComplexityRoot.Album.Thumbnail(childComplexity), true + case "Album.title": + if e.ComplexityRoot.Album.Title == nil { + break + } + + return e.ComplexityRoot.Album.Title(childComplexity), true + + case "AuthorizeResult.status": + if e.ComplexityRoot.AuthorizeResult.Status == nil { + break + } + + return e.ComplexityRoot.AuthorizeResult.Status(childComplexity), true + case "AuthorizeResult.success": + if e.ComplexityRoot.AuthorizeResult.Success == nil { + break + } + + return e.ComplexityRoot.AuthorizeResult.Success(childComplexity), true + case "AuthorizeResult.token": + if e.ComplexityRoot.AuthorizeResult.Token == nil { + break + } + + return e.ComplexityRoot.AuthorizeResult.Token(childComplexity), true + + case "Coordinates.latitude": + if e.ComplexityRoot.Coordinates.Latitude == nil { + break + } + + return e.ComplexityRoot.Coordinates.Latitude(childComplexity), true + case "Coordinates.longitude": + if e.ComplexityRoot.Coordinates.Longitude == nil { + break + } + + return e.ComplexityRoot.Coordinates.Longitude(childComplexity), true + + case "FaceGroup.id": + if e.ComplexityRoot.FaceGroup.ID == nil { + break + } + + return e.ComplexityRoot.FaceGroup.ID(childComplexity), true + case "FaceGroup.imageFaceCount": + if e.ComplexityRoot.FaceGroup.ImageFaceCount == nil { + break + } + + return e.ComplexityRoot.FaceGroup.ImageFaceCount(childComplexity), true + case "FaceGroup.imageFaces": + if e.ComplexityRoot.FaceGroup.ImageFaces == nil { + break + } + + args, err := ec.field_FaceGroup_imageFaces_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.FaceGroup.ImageFaces(childComplexity, args["paginate"].(*models.Pagination)), true + case "FaceGroup.label": + if e.ComplexityRoot.FaceGroup.Label == nil { + break + } + + return e.ComplexityRoot.FaceGroup.Label(childComplexity), true + + case "FaceRectangle.maxX": + if e.ComplexityRoot.FaceRectangle.MaxX == nil { + break + } + + return e.ComplexityRoot.FaceRectangle.MaxX(childComplexity), true + case "FaceRectangle.maxY": + if e.ComplexityRoot.FaceRectangle.MaxY == nil { + break + } + + return e.ComplexityRoot.FaceRectangle.MaxY(childComplexity), true + case "FaceRectangle.minX": + if e.ComplexityRoot.FaceRectangle.MinX == nil { + break + } + + return e.ComplexityRoot.FaceRectangle.MinX(childComplexity), true + case "FaceRectangle.minY": + if e.ComplexityRoot.FaceRectangle.MinY == nil { + break + } + + return e.ComplexityRoot.FaceRectangle.MinY(childComplexity), true + + case "ImageFace.faceGroup": + if e.ComplexityRoot.ImageFace.FaceGroup == nil { + break + } + + return e.ComplexityRoot.ImageFace.FaceGroup(childComplexity), true + case "ImageFace.id": + if e.ComplexityRoot.ImageFace.ID == nil { + break + } + + return e.ComplexityRoot.ImageFace.ID(childComplexity), true + case "ImageFace.media": + if e.ComplexityRoot.ImageFace.Media == nil { + break + } + + return e.ComplexityRoot.ImageFace.Media(childComplexity), true + case "ImageFace.rectangle": + if e.ComplexityRoot.ImageFace.Rectangle == nil { + break + } + + return e.ComplexityRoot.ImageFace.Rectangle(childComplexity), true + + case "Media.album": + if e.ComplexityRoot.Media.Album == nil { + break + } + + return e.ComplexityRoot.Media.Album(childComplexity), true + case "Media.blurhash": + if e.ComplexityRoot.Media.Blurhash == nil { + break + } + + return e.ComplexityRoot.Media.Blurhash(childComplexity), true + case "Media.date": + if e.ComplexityRoot.Media.Date == nil { + break + } + + return e.ComplexityRoot.Media.Date(childComplexity), true + case "Media.downloads": + if e.ComplexityRoot.Media.Downloads == nil { + break + } + + return e.ComplexityRoot.Media.Downloads(childComplexity), true + case "Media.exif": + if e.ComplexityRoot.Media.Exif == nil { + break + } + + return e.ComplexityRoot.Media.Exif(childComplexity), true + case "Media.faces": + if e.ComplexityRoot.Media.Faces == nil { + break + } + + return e.ComplexityRoot.Media.Faces(childComplexity), true + case "Media.favorite": + if e.ComplexityRoot.Media.Favorite == nil { + break + } + + return e.ComplexityRoot.Media.Favorite(childComplexity), true + case "Media.highRes": + if e.ComplexityRoot.Media.HighRes == nil { + break + } + + return e.ComplexityRoot.Media.HighRes(childComplexity), true + case "Media.id": + if e.ComplexityRoot.Media.ID == nil { + break + } + + return e.ComplexityRoot.Media.ID(childComplexity), true + case "Media.path": + if e.ComplexityRoot.Media.Path == nil { + break + } + + return e.ComplexityRoot.Media.Path(childComplexity), true + case "Media.shares": + if e.ComplexityRoot.Media.Shares == nil { + break + } + + return e.ComplexityRoot.Media.Shares(childComplexity), true + case "Media.thumbnail": + if e.ComplexityRoot.Media.Thumbnail == nil { + break + } + + return e.ComplexityRoot.Media.Thumbnail(childComplexity), true + case "Media.title": + if e.ComplexityRoot.Media.Title == nil { + break + } + + return e.ComplexityRoot.Media.Title(childComplexity), true + case "Media.type": + if e.ComplexityRoot.Media.Type == nil { + break + } + + return e.ComplexityRoot.Media.Type(childComplexity), true + case "Media.videoMetadata": + if e.ComplexityRoot.Media.VideoMetadata == nil { + break + } + + return e.ComplexityRoot.Media.VideoMetadata(childComplexity), true + case "Media.videoWeb": + if e.ComplexityRoot.Media.VideoWeb == nil { + break + } + + return e.ComplexityRoot.Media.VideoWeb(childComplexity), true + + case "MediaDownload.mediaUrl": + if e.ComplexityRoot.MediaDownload.MediaURL == nil { + break + } + + return e.ComplexityRoot.MediaDownload.MediaURL(childComplexity), true + case "MediaDownload.title": + if e.ComplexityRoot.MediaDownload.Title == nil { + break + } + + return e.ComplexityRoot.MediaDownload.Title(childComplexity), true + + case "MediaEXIF.aperture": + if e.ComplexityRoot.MediaEXIF.Aperture == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Aperture(childComplexity), true + case "MediaEXIF.camera": + if e.ComplexityRoot.MediaEXIF.Camera == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Camera(childComplexity), true + case "MediaEXIF.coordinates": + if e.ComplexityRoot.MediaEXIF.Coordinates == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Coordinates(childComplexity), true + case "MediaEXIF.dateShot": + if e.ComplexityRoot.MediaEXIF.DateShotWithOffset == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.DateShotWithOffset(childComplexity), true + case "MediaEXIF.description": + if e.ComplexityRoot.MediaEXIF.Description == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Description(childComplexity), true + case "MediaEXIF.exposure": + if e.ComplexityRoot.MediaEXIF.Exposure == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Exposure(childComplexity), true + case "MediaEXIF.exposureProgram": + if e.ComplexityRoot.MediaEXIF.ExposureProgram == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.ExposureProgram(childComplexity), true + case "MediaEXIF.flash": + if e.ComplexityRoot.MediaEXIF.Flash == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Flash(childComplexity), true + case "MediaEXIF.focalLength": + if e.ComplexityRoot.MediaEXIF.FocalLength == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.FocalLength(childComplexity), true + case "MediaEXIF.id": + if e.ComplexityRoot.MediaEXIF.ID == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.ID(childComplexity), true + case "MediaEXIF.iso": + if e.ComplexityRoot.MediaEXIF.Iso == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Iso(childComplexity), true + case "MediaEXIF.lens": + if e.ComplexityRoot.MediaEXIF.Lens == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Lens(childComplexity), true + case "MediaEXIF.maker": + if e.ComplexityRoot.MediaEXIF.Maker == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Maker(childComplexity), true + case "MediaEXIF.media": + if e.ComplexityRoot.MediaEXIF.Media == nil { + break + } + + return e.ComplexityRoot.MediaEXIF.Media(childComplexity), true + + case "MediaURL.fileSize": + if e.ComplexityRoot.MediaURL.FileSize == nil { + break + } + + return e.ComplexityRoot.MediaURL.FileSize(childComplexity), true + case "MediaURL.height": + if e.ComplexityRoot.MediaURL.Height == nil { + break + } + + return e.ComplexityRoot.MediaURL.Height(childComplexity), true + case "MediaURL.url": + if e.ComplexityRoot.MediaURL.URL == nil { + break + } + + return e.ComplexityRoot.MediaURL.URL(childComplexity), true + case "MediaURL.width": + if e.ComplexityRoot.MediaURL.Width == nil { + break + } + + return e.ComplexityRoot.MediaURL.Width(childComplexity), true + + case "Mutation.authorizeUser": + if e.ComplexityRoot.Mutation.AuthorizeUser == nil { + break + } + + args, err := ec.field_Mutation_authorizeUser_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.AuthorizeUser(childComplexity, args["username"].(string), args["password"].(string)), true + case "Mutation.changeUserPreferences": + if e.ComplexityRoot.Mutation.ChangeUserPreferences == nil { + break + } + + args, err := ec.field_Mutation_changeUserPreferences_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.ChangeUserPreferences(childComplexity, args["language"].(*string)), true + case "Mutation.combineFaceGroups": + if e.ComplexityRoot.Mutation.CombineFaceGroups == nil { + break + } + + args, err := ec.field_Mutation_combineFaceGroups_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.CombineFaceGroups(childComplexity, args["destinationFaceGroupID"].(int), args["sourceFaceGroupIDs"].([]int)), true + case "Mutation.createUser": + if e.ComplexityRoot.Mutation.CreateUser == nil { + break + } + + args, err := ec.field_Mutation_createUser_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.CreateUser(childComplexity, args["username"].(string), args["password"].(*string), args["admin"].(bool)), true + case "Mutation.deleteShareToken": + if e.ComplexityRoot.Mutation.DeleteShareToken == nil { + break + } + + args, err := ec.field_Mutation_deleteShareToken_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.DeleteShareToken(childComplexity, args["token"].(string)), true + case "Mutation.deleteUser": + if e.ComplexityRoot.Mutation.DeleteUser == nil { + break + } + + args, err := ec.field_Mutation_deleteUser_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.DeleteUser(childComplexity, args["id"].(int)), true + case "Mutation.detachImageFaces": + if e.ComplexityRoot.Mutation.DetachImageFaces == nil { + break + } + + args, err := ec.field_Mutation_detachImageFaces_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.DetachImageFaces(childComplexity, args["imageFaceIDs"].([]int)), true + case "Mutation.favoriteMedia": + if e.ComplexityRoot.Mutation.FavoriteMedia == nil { + break + } + + args, err := ec.field_Mutation_favoriteMedia_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.FavoriteMedia(childComplexity, args["mediaId"].(int), args["favorite"].(bool)), true + case "Mutation.initialSetupWizard": + if e.ComplexityRoot.Mutation.InitialSetupWizard == nil { + break + } + + args, err := ec.field_Mutation_initialSetupWizard_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.InitialSetupWizard(childComplexity, args["username"].(string), args["password"].(string), args["rootPath"].(string)), true + case "Mutation.moveImageFaces": + if e.ComplexityRoot.Mutation.MoveImageFaces == nil { + break + } + + args, err := ec.field_Mutation_moveImageFaces_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.MoveImageFaces(childComplexity, args["imageFaceIDs"].([]int), args["destinationFaceGroupID"].(int)), true + case "Mutation.protectShareToken": + if e.ComplexityRoot.Mutation.ProtectShareToken == nil { + break + } + + args, err := ec.field_Mutation_protectShareToken_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.ProtectShareToken(childComplexity, args["token"].(string), args["password"].(*string)), true + case "Mutation.recognizeUnlabeledFaces": + if e.ComplexityRoot.Mutation.RecognizeUnlabeledFaces == nil { + break + } + + return e.ComplexityRoot.Mutation.RecognizeUnlabeledFaces(childComplexity), true + case "Mutation.resetAlbumCover": + if e.ComplexityRoot.Mutation.ResetAlbumCover == nil { + break + } + + args, err := ec.field_Mutation_resetAlbumCover_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.ResetAlbumCover(childComplexity, args["albumID"].(int)), true + case "Mutation.scanAll": + if e.ComplexityRoot.Mutation.ScanAll == nil { + break + } + + return e.ComplexityRoot.Mutation.ScanAll(childComplexity), true + case "Mutation.scanUser": + if e.ComplexityRoot.Mutation.ScanUser == nil { + break + } + + args, err := ec.field_Mutation_scanUser_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.ScanUser(childComplexity, args["userId"].(int)), true + case "Mutation.setAlbumCover": + if e.ComplexityRoot.Mutation.SetAlbumCover == nil { + break + } + + args, err := ec.field_Mutation_setAlbumCover_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.SetAlbumCover(childComplexity, args["coverID"].(int)), true + case "Mutation.setExpireShareToken": + if e.ComplexityRoot.Mutation.SetExpireShareToken == nil { + break + } + + args, err := ec.field_Mutation_setExpireShareToken_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.SetExpireShareToken(childComplexity, args["token"].(string), args["expire"].(*time.Time)), true + case "Mutation.setFaceGroupLabel": + if e.ComplexityRoot.Mutation.SetFaceGroupLabel == nil { + break + } + + args, err := ec.field_Mutation_setFaceGroupLabel_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.SetFaceGroupLabel(childComplexity, args["faceGroupID"].(int), args["label"].(*string)), true + case "Mutation.setPeriodicScanInterval": + if e.ComplexityRoot.Mutation.SetPeriodicScanInterval == nil { + break + } + + args, err := ec.field_Mutation_setPeriodicScanInterval_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.SetPeriodicScanInterval(childComplexity, args["interval"].(int)), true + case "Mutation.setScannerConcurrentWorkers": + if e.ComplexityRoot.Mutation.SetScannerConcurrentWorkers == nil { + break + } + + args, err := ec.field_Mutation_setScannerConcurrentWorkers_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.SetScannerConcurrentWorkers(childComplexity, args["workers"].(int)), true + case "Mutation.shareAlbum": + if e.ComplexityRoot.Mutation.ShareAlbum == nil { + break + } + + args, err := ec.field_Mutation_shareAlbum_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.ShareAlbum(childComplexity, args["albumId"].(int), args["expire"].(*time.Time), args["password"].(*string)), true + case "Mutation.shareMedia": + if e.ComplexityRoot.Mutation.ShareMedia == nil { + break + } + + args, err := ec.field_Mutation_shareMedia_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.ShareMedia(childComplexity, args["mediaId"].(int), args["expire"].(*time.Time), args["password"].(*string)), true + case "Mutation.updateUser": + if e.ComplexityRoot.Mutation.UpdateUser == nil { + break + } + + args, err := ec.field_Mutation_updateUser_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.UpdateUser(childComplexity, args["id"].(int), args["username"].(*string), args["password"].(*string), args["admin"].(*bool)), true + case "Mutation.userAddRootPath": + if e.ComplexityRoot.Mutation.UserAddRootPath == nil { + break + } + + args, err := ec.field_Mutation_userAddRootPath_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.UserAddRootPath(childComplexity, args["id"].(int), args["rootPath"].(string)), true + case "Mutation.userRemoveRootAlbum": + if e.ComplexityRoot.Mutation.UserRemoveRootAlbum == nil { + break + } + + args, err := ec.field_Mutation_userRemoveRootAlbum_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.UserRemoveRootAlbum(childComplexity, args["userId"].(int), args["albumId"].(int)), true + + case "Notification.content": + if e.ComplexityRoot.Notification.Content == nil { + break + } + + return e.ComplexityRoot.Notification.Content(childComplexity), true + case "Notification.header": + if e.ComplexityRoot.Notification.Header == nil { + break + } + + return e.ComplexityRoot.Notification.Header(childComplexity), true + case "Notification.key": + if e.ComplexityRoot.Notification.Key == nil { + break + } + + return e.ComplexityRoot.Notification.Key(childComplexity), true + case "Notification.negative": + if e.ComplexityRoot.Notification.Negative == nil { + break + } + + return e.ComplexityRoot.Notification.Negative(childComplexity), true + case "Notification.positive": + if e.ComplexityRoot.Notification.Positive == nil { + break + } + + return e.ComplexityRoot.Notification.Positive(childComplexity), true + case "Notification.progress": + if e.ComplexityRoot.Notification.Progress == nil { + break + } + + return e.ComplexityRoot.Notification.Progress(childComplexity), true + case "Notification.timeout": + if e.ComplexityRoot.Notification.Timeout == nil { + break + } + + return e.ComplexityRoot.Notification.Timeout(childComplexity), true + case "Notification.type": + if e.ComplexityRoot.Notification.Type == nil { + break + } + + return e.ComplexityRoot.Notification.Type(childComplexity), true + + case "Query.album": + if e.ComplexityRoot.Query.Album == nil { + break + } + + args, err := ec.field_Query_album_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.Album(childComplexity, args["id"].(int), args["tokenCredentials"].(*models.ShareTokenCredentials)), true + case "Query.faceGroup": + if e.ComplexityRoot.Query.FaceGroup == nil { + break + } + + args, err := ec.field_Query_faceGroup_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.FaceGroup(childComplexity, args["id"].(int)), true + + case "Query.mapboxToken": + if e.ComplexityRoot.Query.MapboxToken == nil { + break + } + + return e.ComplexityRoot.Query.MapboxToken(childComplexity), true + case "Query.media": + if e.ComplexityRoot.Query.Media == nil { + break + } + + args, err := ec.field_Query_media_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.Media(childComplexity, args["id"].(int), args["tokenCredentials"].(*models.ShareTokenCredentials)), true + case "Query.mediaList": + if e.ComplexityRoot.Query.MediaList == nil { + break + } + + args, err := ec.field_Query_mediaList_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.MediaList(childComplexity, args["ids"].([]int)), true + case "Query.myAlbums": + if e.ComplexityRoot.Query.MyAlbums == nil { + break + } + + args, err := ec.field_Query_myAlbums_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.MyAlbums(childComplexity, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination), args["onlyRoot"].(*bool), args["showEmpty"].(*bool), args["onlyWithFavorites"].(*bool)), true + case "Query.myFaceGroups": + if e.ComplexityRoot.Query.MyFaceGroups == nil { + break + } + + args, err := ec.field_Query_myFaceGroups_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.MyFaceGroups(childComplexity, args["paginate"].(*models.Pagination)), true + case "Query.myMedia": + if e.ComplexityRoot.Query.MyMedia == nil { + break + } + + args, err := ec.field_Query_myMedia_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.MyMedia(childComplexity, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination)), true + case "Query.myMediaGeoJson": + if e.ComplexityRoot.Query.MyMediaGeoJSON == nil { + break + } + + return e.ComplexityRoot.Query.MyMediaGeoJSON(childComplexity), true + case "Query.myTimeline": + if e.ComplexityRoot.Query.MyTimeline == nil { + break + } + + args, err := ec.field_Query_myTimeline_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.MyTimeline(childComplexity, args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool), args["fromDate"].(*time.Time)), true + case "Query.myUser": + if e.ComplexityRoot.Query.MyUser == nil { + break + } + + return e.ComplexityRoot.Query.MyUser(childComplexity), true + case "Query.myUserPreferences": + if e.ComplexityRoot.Query.MyUserPreferences == nil { + break + } + + return e.ComplexityRoot.Query.MyUserPreferences(childComplexity), true + case "Query.search": + if e.ComplexityRoot.Query.Search == nil { + break + } + + args, err := ec.field_Query_search_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.Search(childComplexity, args["query"].(string), args["limitMedia"].(*int), args["limitAlbums"].(*int)), true + case "Query.shareToken": + if e.ComplexityRoot.Query.ShareToken == nil { + break + } + + args, err := ec.field_Query_shareToken_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.ShareToken(childComplexity, args["credentials"].(models.ShareTokenCredentials)), true + case "Query.shareTokenValidatePassword": + if e.ComplexityRoot.Query.ShareTokenValidatePassword == nil { + break + } + + args, err := ec.field_Query_shareTokenValidatePassword_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.ShareTokenValidatePassword(childComplexity, args["credentials"].(models.ShareTokenCredentials)), true + case "Query.siteInfo": + if e.ComplexityRoot.Query.SiteInfo == nil { + break + } + + return e.ComplexityRoot.Query.SiteInfo(childComplexity), true + case "Query.user": + if e.ComplexityRoot.Query.User == nil { + break + } + + args, err := ec.field_Query_user_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Query.User(childComplexity, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination)), true + + case "ScannerResult.finished": + if e.ComplexityRoot.ScannerResult.Finished == nil { + break + } + + return e.ComplexityRoot.ScannerResult.Finished(childComplexity), true + case "ScannerResult.message": + if e.ComplexityRoot.ScannerResult.Message == nil { + break + } + + return e.ComplexityRoot.ScannerResult.Message(childComplexity), true + case "ScannerResult.progress": + if e.ComplexityRoot.ScannerResult.Progress == nil { + break + } + + return e.ComplexityRoot.ScannerResult.Progress(childComplexity), true + case "ScannerResult.success": + if e.ComplexityRoot.ScannerResult.Success == nil { + break + } + + return e.ComplexityRoot.ScannerResult.Success(childComplexity), true + + case "SearchResult.albums": + if e.ComplexityRoot.SearchResult.Albums == nil { + break + } + + return e.ComplexityRoot.SearchResult.Albums(childComplexity), true + case "SearchResult.media": + if e.ComplexityRoot.SearchResult.Media == nil { + break + } + + return e.ComplexityRoot.SearchResult.Media(childComplexity), true + case "SearchResult.query": + if e.ComplexityRoot.SearchResult.Query == nil { + break + } + + return e.ComplexityRoot.SearchResult.Query(childComplexity), true + + case "ShareToken.album": + if e.ComplexityRoot.ShareToken.Album == nil { + break + } + + return e.ComplexityRoot.ShareToken.Album(childComplexity), true + case "ShareToken.expire": + if e.ComplexityRoot.ShareToken.Expire == nil { + break + } + + return e.ComplexityRoot.ShareToken.Expire(childComplexity), true + case "ShareToken.hasPassword": + if e.ComplexityRoot.ShareToken.HasPassword == nil { + break + } + + return e.ComplexityRoot.ShareToken.HasPassword(childComplexity), true + case "ShareToken.id": + if e.ComplexityRoot.ShareToken.ID == nil { + break + } + + return e.ComplexityRoot.ShareToken.ID(childComplexity), true + case "ShareToken.media": + if e.ComplexityRoot.ShareToken.Media == nil { + break + } + + return e.ComplexityRoot.ShareToken.Media(childComplexity), true + case "ShareToken.owner": + if e.ComplexityRoot.ShareToken.Owner == nil { + break + } + + return e.ComplexityRoot.ShareToken.Owner(childComplexity), true + case "ShareToken.token": + if e.ComplexityRoot.ShareToken.Token == nil { + break + } + + return e.ComplexityRoot.ShareToken.Token(childComplexity), true + + case "SiteInfo.concurrentWorkers": + if e.ComplexityRoot.SiteInfo.ConcurrentWorkers == nil { + break + } + + return e.ComplexityRoot.SiteInfo.ConcurrentWorkers(childComplexity), true + case "SiteInfo.faceDetectionEnabled": + if e.ComplexityRoot.SiteInfo.FaceDetectionEnabled == nil { + break + } + + return e.ComplexityRoot.SiteInfo.FaceDetectionEnabled(childComplexity), true + case "SiteInfo.initialSetup": + if e.ComplexityRoot.SiteInfo.InitialSetup == nil { + break + } + + return e.ComplexityRoot.SiteInfo.InitialSetup(childComplexity), true + case "SiteInfo.periodicScanInterval": + if e.ComplexityRoot.SiteInfo.PeriodicScanInterval == nil { + break + } + + return e.ComplexityRoot.SiteInfo.PeriodicScanInterval(childComplexity), true + + case "Subscription.notification": + if e.ComplexityRoot.Subscription.Notification == nil { + break + } + + return e.ComplexityRoot.Subscription.Notification(childComplexity), true + + case "TimelineGroup.album": + if e.ComplexityRoot.TimelineGroup.Album == nil { + break + } + + return e.ComplexityRoot.TimelineGroup.Album(childComplexity), true + case "TimelineGroup.date": + if e.ComplexityRoot.TimelineGroup.Date == nil { + break + } + + return e.ComplexityRoot.TimelineGroup.Date(childComplexity), true + case "TimelineGroup.media": + if e.ComplexityRoot.TimelineGroup.Media == nil { + break + } + + return e.ComplexityRoot.TimelineGroup.Media(childComplexity), true + case "TimelineGroup.mediaTotal": + if e.ComplexityRoot.TimelineGroup.MediaTotal == nil { + break + } + + return e.ComplexityRoot.TimelineGroup.MediaTotal(childComplexity), true + + case "User.admin": + if e.ComplexityRoot.User.Admin == nil { + break + } + + return e.ComplexityRoot.User.Admin(childComplexity), true + case "User.albums": + if e.ComplexityRoot.User.Albums == nil { + break + } + + return e.ComplexityRoot.User.Albums(childComplexity), true + case "User.id": + if e.ComplexityRoot.User.ID == nil { + break + } + + return e.ComplexityRoot.User.ID(childComplexity), true + case "User.rootAlbums": + if e.ComplexityRoot.User.RootAlbums == nil { + break + } + + return e.ComplexityRoot.User.RootAlbums(childComplexity), true + case "User.username": + if e.ComplexityRoot.User.Username == nil { + break + } + + return e.ComplexityRoot.User.Username(childComplexity), true + + case "UserPreferences.id": + if e.ComplexityRoot.UserPreferences.ID == nil { + break + } + + return e.ComplexityRoot.UserPreferences.ID(childComplexity), true + case "UserPreferences.language": + if e.ComplexityRoot.UserPreferences.Language == nil { + break + } + + return e.ComplexityRoot.UserPreferences.Language(childComplexity), true + + case "VideoMetadata.audio": + if e.ComplexityRoot.VideoMetadata.Audio == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Audio(childComplexity), true + case "VideoMetadata.bitrate": + if e.ComplexityRoot.VideoMetadata.Bitrate == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Bitrate(childComplexity), true + case "VideoMetadata.codec": + if e.ComplexityRoot.VideoMetadata.Codec == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Codec(childComplexity), true + case "VideoMetadata.colorProfile": + if e.ComplexityRoot.VideoMetadata.ColorProfile == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.ColorProfile(childComplexity), true + case "VideoMetadata.duration": + if e.ComplexityRoot.VideoMetadata.Duration == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Duration(childComplexity), true + case "VideoMetadata.framerate": + if e.ComplexityRoot.VideoMetadata.Framerate == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Framerate(childComplexity), true + case "VideoMetadata.height": + if e.ComplexityRoot.VideoMetadata.Height == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Height(childComplexity), true + case "VideoMetadata.id": + if e.ComplexityRoot.VideoMetadata.ID == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.ID(childComplexity), true + case "VideoMetadata.media": + if e.ComplexityRoot.VideoMetadata.Media == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Media(childComplexity), true + case "VideoMetadata.width": + if e.ComplexityRoot.VideoMetadata.Width == nil { + break + } + + return e.ComplexityRoot.VideoMetadata.Width(childComplexity), true + + } + return 0, false +} + +func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { + opCtx := graphql.GetOperationContext(ctx) + ec := newExecutionContext(opCtx, e, make(chan graphql.DeferredResult)) + inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputOrdering, + ec.unmarshalInputPagination, + ec.unmarshalInputShareTokenCredentials, + ) + first := true + + switch opCtx.Operation.Operation { + case ast.Query: + return func(ctx context.Context) *graphql.Response { + var response graphql.Response + var data graphql.Marshaler + if first { + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data = ec._Query(ctx, opCtx.Operation.SelectionSet) + } else { + if atomic.LoadInt32(&ec.PendingDeferred) > 0 { + result := <-ec.DeferredResults + atomic.AddInt32(&ec.PendingDeferred, -1) + data = result.Result + response.Path = result.Path + response.Label = result.Label + response.Errors = result.Errors + } else { + return nil + } + } + var buf bytes.Buffer + data.MarshalGQL(&buf) + response.Data = buf.Bytes() + if atomic.LoadInt32(&ec.Deferred) > 0 { + hasNext := atomic.LoadInt32(&ec.PendingDeferred) > 0 + response.HasNext = &hasNext + } + + return &response + } + case ast.Mutation: + return func(ctx context.Context) *graphql.Response { + if !first { + return nil + } + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data := ec._Mutation(ctx, opCtx.Operation.SelectionSet) + var buf bytes.Buffer + data.MarshalGQL(&buf) + + return &graphql.Response{ + Data: buf.Bytes(), + } + } + case ast.Subscription: + next := ec._Subscription(ctx, opCtx.Operation.SelectionSet) + + var buf bytes.Buffer + return func(ctx context.Context) *graphql.Response { + buf.Reset() + data := next(ctx) + + if data == nil { + return nil + } + data.MarshalGQL(&buf) + + return &graphql.Response{ + Data: buf.Bytes(), + } + } + + default: + return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation")) + } +} + +type executionContext struct { + *graphql.ExecutionContextState[ResolverRoot, DirectiveRoot, ComplexityRoot] +} + +func newExecutionContext( + opCtx *graphql.OperationContext, + execSchema *executableSchema, + deferredResults chan graphql.DeferredResult, +) *executionContext { + return &executionContext{ + ExecutionContextState: graphql.NewExecutionContextState[ResolverRoot, DirectiveRoot, ComplexityRoot]( + opCtx, + (*graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot])(execSchema), + parsedSchema, + deferredResults, + ), + } +} + +//go:embed "resolvers/album.graphql" "resolvers/faces.graphql" "resolvers/media.graphql" "resolvers/media_geo_json.graphql" "resolvers/notification.graphql" "resolvers/root.graphql" "resolvers/scanner.graphql" "resolvers/search.graphql" "resolvers/share_token.graphql" "resolvers/site_info.graphql" "resolvers/timeline.graphql" "resolvers/user.graphql" +var sourcesFS embed.FS + +func sourceData(filename string) string { + data, err := sourcesFS.ReadFile(filename) + if err != nil { + panic(fmt.Sprintf("codegen problem: %s not available", filename)) + } + return string(data) +} + +var sources = []*ast.Source{ + {Name: "resolvers/album.graphql", Input: sourceData("resolvers/album.graphql"), BuiltIn: false}, + {Name: "resolvers/faces.graphql", Input: sourceData("resolvers/faces.graphql"), BuiltIn: false}, + {Name: "resolvers/media.graphql", Input: sourceData("resolvers/media.graphql"), BuiltIn: false}, + {Name: "resolvers/media_geo_json.graphql", Input: sourceData("resolvers/media_geo_json.graphql"), BuiltIn: false}, + {Name: "resolvers/notification.graphql", Input: sourceData("resolvers/notification.graphql"), BuiltIn: false}, + {Name: "resolvers/root.graphql", Input: sourceData("resolvers/root.graphql"), BuiltIn: false}, + {Name: "resolvers/scanner.graphql", Input: sourceData("resolvers/scanner.graphql"), BuiltIn: false}, + {Name: "resolvers/search.graphql", Input: sourceData("resolvers/search.graphql"), BuiltIn: false}, + {Name: "resolvers/share_token.graphql", Input: sourceData("resolvers/share_token.graphql"), BuiltIn: false}, + {Name: "resolvers/site_info.graphql", Input: sourceData("resolvers/site_info.graphql"), BuiltIn: false}, + {Name: "resolvers/timeline.graphql", Input: sourceData("resolvers/timeline.graphql"), BuiltIn: false}, + {Name: "resolvers/user.graphql", Input: sourceData("resolvers/user.graphql"), BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) + +// childFields_* functions provide shared child field context lookups. +// Each function is generated once per unique object type, deduplicating the +// switch statements that were previously inlined in every fieldContext_* function. + +func (ec *executionContext) childFields_Album(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Album_id(ctx, field) + case "title": + return ec.fieldContext_Album_title(ctx, field) + case "media": + return ec.fieldContext_Album_media(ctx, field) + case "subAlbums": + return ec.fieldContext_Album_subAlbums(ctx, field) + case "parentAlbum": + return ec.fieldContext_Album_parentAlbum(ctx, field) + case "owner": + return ec.fieldContext_Album_owner(ctx, field) + case "filePath": + return ec.fieldContext_Album_filePath(ctx, field) + case "thumbnail": + return ec.fieldContext_Album_thumbnail(ctx, field) + case "path": + return ec.fieldContext_Album_path(ctx, field) + case "shares": + return ec.fieldContext_Album_shares(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Album", field.Name) +} + +func (ec *executionContext) childFields_AuthorizeResult(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "success": + return ec.fieldContext_AuthorizeResult_success(ctx, field) + case "status": + return ec.fieldContext_AuthorizeResult_status(ctx, field) + case "token": + return ec.fieldContext_AuthorizeResult_token(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AuthorizeResult", field.Name) +} + +func (ec *executionContext) childFields_Coordinates(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "latitude": + return ec.fieldContext_Coordinates_latitude(ctx, field) + case "longitude": + return ec.fieldContext_Coordinates_longitude(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Coordinates", field.Name) +} + +func (ec *executionContext) childFields_FaceGroup(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_FaceGroup_id(ctx, field) + case "label": + return ec.fieldContext_FaceGroup_label(ctx, field) + case "imageFaces": + return ec.fieldContext_FaceGroup_imageFaces(ctx, field) + case "imageFaceCount": + return ec.fieldContext_FaceGroup_imageFaceCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type FaceGroup", field.Name) +} + +func (ec *executionContext) childFields_FaceRectangle(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "minX": + return ec.fieldContext_FaceRectangle_minX(ctx, field) + case "maxX": + return ec.fieldContext_FaceRectangle_maxX(ctx, field) + case "minY": + return ec.fieldContext_FaceRectangle_minY(ctx, field) + case "maxY": + return ec.fieldContext_FaceRectangle_maxY(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type FaceRectangle", field.Name) +} + +func (ec *executionContext) childFields_ImageFace(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ImageFace_id(ctx, field) + case "media": + return ec.fieldContext_ImageFace_media(ctx, field) + case "rectangle": + return ec.fieldContext_ImageFace_rectangle(ctx, field) + case "faceGroup": + return ec.fieldContext_ImageFace_faceGroup(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageFace", field.Name) +} + +func (ec *executionContext) childFields_Media(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Media_id(ctx, field) + case "title": + return ec.fieldContext_Media_title(ctx, field) + case "path": + return ec.fieldContext_Media_path(ctx, field) + case "thumbnail": + return ec.fieldContext_Media_thumbnail(ctx, field) + case "highRes": + return ec.fieldContext_Media_highRes(ctx, field) + case "videoWeb": + return ec.fieldContext_Media_videoWeb(ctx, field) + case "album": + return ec.fieldContext_Media_album(ctx, field) + case "exif": + return ec.fieldContext_Media_exif(ctx, field) + case "videoMetadata": + return ec.fieldContext_Media_videoMetadata(ctx, field) + case "favorite": + return ec.fieldContext_Media_favorite(ctx, field) + case "type": + return ec.fieldContext_Media_type(ctx, field) + case "date": + return ec.fieldContext_Media_date(ctx, field) + case "blurhash": + return ec.fieldContext_Media_blurhash(ctx, field) + case "shares": + return ec.fieldContext_Media_shares(ctx, field) + case "downloads": + return ec.fieldContext_Media_downloads(ctx, field) + case "faces": + return ec.fieldContext_Media_faces(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Media", field.Name) +} + +func (ec *executionContext) childFields_MediaDownload(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "title": + return ec.fieldContext_MediaDownload_title(ctx, field) + case "mediaUrl": + return ec.fieldContext_MediaDownload_mediaUrl(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type MediaDownload", field.Name) +} + +func (ec *executionContext) childFields_MediaEXIF(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_MediaEXIF_id(ctx, field) + case "media": + return ec.fieldContext_MediaEXIF_media(ctx, field) + case "description": + return ec.fieldContext_MediaEXIF_description(ctx, field) + case "camera": + return ec.fieldContext_MediaEXIF_camera(ctx, field) + case "maker": + return ec.fieldContext_MediaEXIF_maker(ctx, field) + case "lens": + return ec.fieldContext_MediaEXIF_lens(ctx, field) + case "dateShot": + return ec.fieldContext_MediaEXIF_dateShot(ctx, field) + case "exposure": + return ec.fieldContext_MediaEXIF_exposure(ctx, field) + case "aperture": + return ec.fieldContext_MediaEXIF_aperture(ctx, field) + case "iso": + return ec.fieldContext_MediaEXIF_iso(ctx, field) + case "focalLength": + return ec.fieldContext_MediaEXIF_focalLength(ctx, field) + case "flash": + return ec.fieldContext_MediaEXIF_flash(ctx, field) + case "exposureProgram": + return ec.fieldContext_MediaEXIF_exposureProgram(ctx, field) + case "coordinates": + return ec.fieldContext_MediaEXIF_coordinates(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type MediaEXIF", field.Name) +} + +func (ec *executionContext) childFields_MediaURL(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "url": + return ec.fieldContext_MediaURL_url(ctx, field) + case "width": + return ec.fieldContext_MediaURL_width(ctx, field) + case "height": + return ec.fieldContext_MediaURL_height(ctx, field) + case "fileSize": + return ec.fieldContext_MediaURL_fileSize(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type MediaURL", field.Name) +} + +func (ec *executionContext) childFields_Notification(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "key": + return ec.fieldContext_Notification_key(ctx, field) + case "type": + return ec.fieldContext_Notification_type(ctx, field) + case "header": + return ec.fieldContext_Notification_header(ctx, field) + case "content": + return ec.fieldContext_Notification_content(ctx, field) + case "progress": + return ec.fieldContext_Notification_progress(ctx, field) + case "positive": + return ec.fieldContext_Notification_positive(ctx, field) + case "negative": + return ec.fieldContext_Notification_negative(ctx, field) + case "timeout": + return ec.fieldContext_Notification_timeout(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Notification", field.Name) +} + +func (ec *executionContext) childFields_ScannerResult(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "finished": + return ec.fieldContext_ScannerResult_finished(ctx, field) + case "success": + return ec.fieldContext_ScannerResult_success(ctx, field) + case "progress": + return ec.fieldContext_ScannerResult_progress(ctx, field) + case "message": + return ec.fieldContext_ScannerResult_message(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ScannerResult", field.Name) +} + +func (ec *executionContext) childFields_SearchResult(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "query": + return ec.fieldContext_SearchResult_query(ctx, field) + case "albums": + return ec.fieldContext_SearchResult_albums(ctx, field) + case "media": + return ec.fieldContext_SearchResult_media(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SearchResult", field.Name) +} + +func (ec *executionContext) childFields_ShareToken(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ShareToken_id(ctx, field) + case "token": + return ec.fieldContext_ShareToken_token(ctx, field) + case "owner": + return ec.fieldContext_ShareToken_owner(ctx, field) + case "expire": + return ec.fieldContext_ShareToken_expire(ctx, field) + case "hasPassword": + return ec.fieldContext_ShareToken_hasPassword(ctx, field) + case "album": + return ec.fieldContext_ShareToken_album(ctx, field) + case "media": + return ec.fieldContext_ShareToken_media(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ShareToken", field.Name) +} + +func (ec *executionContext) childFields_SiteInfo(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "initialSetup": + return ec.fieldContext_SiteInfo_initialSetup(ctx, field) + case "faceDetectionEnabled": + return ec.fieldContext_SiteInfo_faceDetectionEnabled(ctx, field) + case "periodicScanInterval": + return ec.fieldContext_SiteInfo_periodicScanInterval(ctx, field) + case "concurrentWorkers": + return ec.fieldContext_SiteInfo_concurrentWorkers(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SiteInfo", field.Name) +} + +func (ec *executionContext) childFields_User(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_User_id(ctx, field) + case "username": + return ec.fieldContext_User_username(ctx, field) + case "albums": + return ec.fieldContext_User_albums(ctx, field) + case "rootAlbums": + return ec.fieldContext_User_rootAlbums(ctx, field) + case "admin": + return ec.fieldContext_User_admin(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type User", field.Name) +} + +func (ec *executionContext) childFields_UserPreferences(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_UserPreferences_id(ctx, field) + case "language": + return ec.fieldContext_UserPreferences_language(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type UserPreferences", field.Name) +} + +func (ec *executionContext) childFields_VideoMetadata(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_VideoMetadata_id(ctx, field) + case "media": + return ec.fieldContext_VideoMetadata_media(ctx, field) + case "width": + return ec.fieldContext_VideoMetadata_width(ctx, field) + case "height": + return ec.fieldContext_VideoMetadata_height(ctx, field) + case "duration": + return ec.fieldContext_VideoMetadata_duration(ctx, field) + case "codec": + return ec.fieldContext_VideoMetadata_codec(ctx, field) + case "framerate": + return ec.fieldContext_VideoMetadata_framerate(ctx, field) + case "bitrate": + return ec.fieldContext_VideoMetadata_bitrate(ctx, field) + case "colorProfile": + return ec.fieldContext_VideoMetadata_colorProfile(ctx, field) + case "audio": + return ec.fieldContext_VideoMetadata_audio(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type VideoMetadata", field.Name) +} + +func (ec *executionContext) childFields___Directive(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Directive_name(ctx, field) + case "description": + return ec.fieldContext___Directive_description(ctx, field) + case "isRepeatable": + return ec.fieldContext___Directive_isRepeatable(ctx, field) + case "locations": + return ec.fieldContext___Directive_locations(ctx, field) + case "args": + return ec.fieldContext___Directive_args(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Directive", field.Name) +} + +func (ec *executionContext) childFields___EnumValue(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___EnumValue_name(ctx, field) + case "description": + return ec.fieldContext___EnumValue_description(ctx, field) + case "isDeprecated": + return ec.fieldContext___EnumValue_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___EnumValue_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __EnumValue", field.Name) +} + +func (ec *executionContext) childFields___Field(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Field_name(ctx, field) + case "description": + return ec.fieldContext___Field_description(ctx, field) + case "args": + return ec.fieldContext___Field_args(ctx, field) + case "type": + return ec.fieldContext___Field_type(ctx, field) + case "isDeprecated": + return ec.fieldContext___Field_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___Field_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Field", field.Name) +} + +func (ec *executionContext) childFields___InputValue(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___InputValue_name(ctx, field) + case "description": + return ec.fieldContext___InputValue_description(ctx, field) + case "type": + return ec.fieldContext___InputValue_type(ctx, field) + case "defaultValue": + return ec.fieldContext___InputValue_defaultValue(ctx, field) + case "isDeprecated": + return ec.fieldContext___InputValue_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___InputValue_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) +} + +func (ec *executionContext) childFields___Schema(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "description": + return ec.fieldContext___Schema_description(ctx, field) + case "types": + return ec.fieldContext___Schema_types(ctx, field) + case "queryType": + return ec.fieldContext___Schema_queryType(ctx, field) + case "mutationType": + return ec.fieldContext___Schema_mutationType(ctx, field) + case "subscriptionType": + return ec.fieldContext___Schema_subscriptionType(ctx, field) + case "directives": + return ec.fieldContext___Schema_directives(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Schema", field.Name) +} + +func (ec *executionContext) childFields___Type(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "isOneOf": + return ec.fieldContext___Type_isOneOf(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) +} + +// endregion ************************** generated!.gotpl ************************** + +// region ***************************** args.gotpl ***************************** + +func (ec *executionContext) field_Album_media_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "order", + func(ctx context.Context, v any) (*models.Ordering, error) { + return ec.unmarshalOOrdering2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrdering(ctx, v) + }) + if err != nil { + return nil, err + } + args["order"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "onlyFavorites", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["onlyFavorites"] = arg2 + return args, nil +} + +func (ec *executionContext) field_Album_subAlbums_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "order", + func(ctx context.Context, v any) (*models.Ordering, error) { + return ec.unmarshalOOrdering2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrdering(ctx, v) + }) + if err != nil { + return nil, err + } + args["order"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg1 + return args, nil +} + +func (ec *executionContext) field_FaceGroup_imageFaces_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_authorizeUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "username", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["username"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "password", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["password"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_changeUserPreferences_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "language", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["language"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_combineFaceGroups_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "destinationFaceGroupID", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["destinationFaceGroupID"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "sourceFaceGroupIDs", + func(ctx context.Context, v any) ([]int, error) { + return ec.unmarshalNID2ᚕintᚄ(ctx, v) + }) + if err != nil { + return nil, err + } + args["sourceFaceGroupIDs"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_createUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "username", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["username"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "password", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["password"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "admin", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalNBoolean2bool(ctx, v) + }) + if err != nil { + return nil, err + } + args["admin"] = arg2 + return args, nil +} + +func (ec *executionContext) field_Mutation_deleteShareToken_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "token", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["token"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_deleteUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_detachImageFaces_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "imageFaceIDs", + func(ctx context.Context, v any) ([]int, error) { + return ec.unmarshalNID2ᚕintᚄ(ctx, v) + }) + if err != nil { + return nil, err + } + args["imageFaceIDs"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_favoriteMedia_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "mediaId", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["mediaId"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "favorite", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalNBoolean2bool(ctx, v) + }) + if err != nil { + return nil, err + } + args["favorite"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_initialSetupWizard_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "username", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["username"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "password", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["password"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "rootPath", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["rootPath"] = arg2 + return args, nil +} + +func (ec *executionContext) field_Mutation_moveImageFaces_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "imageFaceIDs", + func(ctx context.Context, v any) ([]int, error) { + return ec.unmarshalNID2ᚕintᚄ(ctx, v) + }) + if err != nil { + return nil, err + } + args["imageFaceIDs"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "destinationFaceGroupID", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["destinationFaceGroupID"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_protectShareToken_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "token", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["token"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "password", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["password"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_resetAlbumCover_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "albumID", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["albumID"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_scanUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "userId", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["userId"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_setAlbumCover_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "coverID", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["coverID"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_setExpireShareToken_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "token", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["token"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "expire", + func(ctx context.Context, v any) (*time.Time, error) { + return ec.unmarshalOTime2ᚖtimeᚐTime(ctx, v) + }) + if err != nil { + return nil, err + } + args["expire"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_setFaceGroupLabel_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "faceGroupID", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["faceGroupID"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "label", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["label"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_setPeriodicScanInterval_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "interval", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNInt2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["interval"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_setScannerConcurrentWorkers_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "workers", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNInt2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["workers"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_shareAlbum_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "albumId", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["albumId"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "expire", + func(ctx context.Context, v any) (*time.Time, error) { + return ec.unmarshalOTime2ᚖtimeᚐTime(ctx, v) + }) + if err != nil { + return nil, err + } + args["expire"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "password", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["password"] = arg2 + return args, nil +} + +func (ec *executionContext) field_Mutation_shareMedia_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "mediaId", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["mediaId"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "expire", + func(ctx context.Context, v any) (*time.Time, error) { + return ec.unmarshalOTime2ᚖtimeᚐTime(ctx, v) + }) + if err != nil { + return nil, err + } + args["expire"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "password", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["password"] = arg2 + return args, nil +} + +func (ec *executionContext) field_Mutation_updateUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "username", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["username"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "password", + func(ctx context.Context, v any) (*string, error) { + return ec.unmarshalOString2ᚖstring(ctx, v) + }) + if err != nil { + return nil, err + } + args["password"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "admin", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["admin"] = arg3 + return args, nil +} + +func (ec *executionContext) field_Mutation_userAddRootPath_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "rootPath", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["rootPath"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_userRemoveRootAlbum_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "userId", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["userId"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "albumId", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["albumId"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "name", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["name"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_album_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "tokenCredentials", + func(ctx context.Context, v any) (*models.ShareTokenCredentials, error) { + return ec.unmarshalOShareTokenCredentials2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, v) + }) + if err != nil { + return nil, err + } + args["tokenCredentials"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Query_faceGroup_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_mediaList_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "ids", + func(ctx context.Context, v any) ([]int, error) { + return ec.unmarshalNID2ᚕintᚄ(ctx, v) + }) + if err != nil { + return nil, err + } + args["ids"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_media_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", + func(ctx context.Context, v any) (int, error) { + return ec.unmarshalNID2int(ctx, v) + }) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "tokenCredentials", + func(ctx context.Context, v any) (*models.ShareTokenCredentials, error) { + return ec.unmarshalOShareTokenCredentials2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, v) + }) + if err != nil { + return nil, err + } + args["tokenCredentials"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Query_myAlbums_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "order", + func(ctx context.Context, v any) (*models.Ordering, error) { + return ec.unmarshalOOrdering2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrdering(ctx, v) + }) + if err != nil { + return nil, err + } + args["order"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "onlyRoot", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["onlyRoot"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "showEmpty", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["showEmpty"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "onlyWithFavorites", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["onlyWithFavorites"] = arg4 + return args, nil +} + +func (ec *executionContext) field_Query_myFaceGroups_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_myMedia_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "order", + func(ctx context.Context, v any) (*models.Ordering, error) { + return ec.unmarshalOOrdering2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrdering(ctx, v) + }) + if err != nil { + return nil, err + } + args["order"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Query_myTimeline_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "onlyFavorites", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["onlyFavorites"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "fromDate", + func(ctx context.Context, v any) (*time.Time, error) { + return ec.unmarshalOTime2ᚖtimeᚐTime(ctx, v) + }) + if err != nil { + return nil, err + } + args["fromDate"] = arg2 + return args, nil +} + +func (ec *executionContext) field_Query_search_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "query", + func(ctx context.Context, v any) (string, error) { + return ec.unmarshalNString2string(ctx, v) + }) + if err != nil { + return nil, err + } + args["query"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "limitMedia", + func(ctx context.Context, v any) (*int, error) { + return ec.unmarshalOInt2ᚖint(ctx, v) + }) + if err != nil { + return nil, err + } + args["limitMedia"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "limitAlbums", + func(ctx context.Context, v any) (*int, error) { + return ec.unmarshalOInt2ᚖint(ctx, v) + }) + if err != nil { + return nil, err + } + args["limitAlbums"] = arg2 + return args, nil +} + +func (ec *executionContext) field_Query_shareTokenValidatePassword_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "credentials", + func(ctx context.Context, v any) (models.ShareTokenCredentials, error) { + return ec.unmarshalNShareTokenCredentials2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, v) + }) + if err != nil { + return nil, err + } + args["credentials"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_shareToken_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "credentials", + func(ctx context.Context, v any) (models.ShareTokenCredentials, error) { + return ec.unmarshalNShareTokenCredentials2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, v) + }) + if err != nil { + return nil, err + } + args["credentials"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "order", + func(ctx context.Context, v any) (*models.Ordering, error) { + return ec.unmarshalOOrdering2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrdering(ctx, v) + }) + if err != nil { + return nil, err + } + args["order"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "paginate", + func(ctx context.Context, v any) (*models.Pagination, error) { + return ec.unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx, v) + }) + if err != nil { + return nil, err + } + args["paginate"] = arg1 + return args, nil +} + +func (ec *executionContext) field___Directive_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Field_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (*bool, error) { + return ec.unmarshalOBoolean2ᚖbool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalOBoolean2bool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "includeDeprecated", + func(ctx context.Context, v any) (bool, error) { + return ec.unmarshalOBoolean2bool(ctx, v) + }) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} + +// endregion ***************************** args.gotpl ***************************** + +// region ************************** directives.gotpl ************************** + +// endregion ************************** directives.gotpl ************************** + +// region **************************** field.gotpl ***************************** + +func (ec *executionContext) _Album_id(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Album", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _Album_title(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_title(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Title, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_title(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Album", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Album_media(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Album().Media(ctx, obj, fc.Args["order"].(*models.Ordering), fc.Args["paginate"].(*models.Pagination), fc.Args["onlyFavorites"].(*bool)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_media(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Album", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Album_media_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Album_subAlbums(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_subAlbums(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Album().SubAlbums(ctx, obj, fc.Args["order"].(*models.Ordering), fc.Args["paginate"].(*models.Pagination)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_subAlbums(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Album", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Album_subAlbums_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Album_parentAlbum(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_parentAlbum(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ParentAlbum, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Album_parentAlbum(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Album", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Album_owner(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_owner(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Album().Owner(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.User) graphql.Marshaler { + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_owner(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Album", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_User(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Album_filePath(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_filePath(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.FilePath(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_filePath(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Album", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Album_thumbnail(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_thumbnail(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Album().Thumbnail(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Media) graphql.Marshaler { + return ec.marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Album_thumbnail(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Album", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Album_path(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_path(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Album().Path(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Album", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Album_shares(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Album_shares(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Album().Shares(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Album_shares(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Album", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _AuthorizeResult_success(ctx context.Context, field graphql.CollectedField, obj *models.AuthorizeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AuthorizeResult_success(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Success, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AuthorizeResult_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AuthorizeResult", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _AuthorizeResult_status(ctx context.Context, field graphql.CollectedField, obj *models.AuthorizeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AuthorizeResult_status(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Status, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AuthorizeResult_status(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AuthorizeResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _AuthorizeResult_token(ctx context.Context, field graphql.CollectedField, obj *models.AuthorizeResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AuthorizeResult_token(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Token, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_AuthorizeResult_token(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AuthorizeResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Coordinates_latitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Coordinates_latitude(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Latitude, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Coordinates_latitude(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Coordinates", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _Coordinates_longitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Coordinates_longitude(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Longitude, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Coordinates_longitude(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Coordinates", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _FaceGroup_id(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceGroup_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FaceGroup_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FaceGroup", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _FaceGroup_label(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceGroup_label(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Label, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_FaceGroup_label(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FaceGroup", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _FaceGroup_imageFaces(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceGroup_imageFaces(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.FaceGroup().ImageFaces(ctx, obj, fc.Args["paginate"].(*models.Pagination)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.ImageFace) graphql.Marshaler { + return ec.marshalNImageFace2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFaceᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FaceGroup_imageFaces(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "FaceGroup", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ImageFace(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_FaceGroup_imageFaces_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _FaceGroup_imageFaceCount(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceGroup_imageFaceCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.FaceGroup().ImageFaceCount(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FaceGroup_imageFaceCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FaceGroup", field, true, true, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _FaceRectangle_minX(ctx context.Context, field graphql.CollectedField, obj *models.FaceRectangle) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceRectangle_minX(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MinX, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FaceRectangle_minX(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FaceRectangle", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _FaceRectangle_maxX(ctx context.Context, field graphql.CollectedField, obj *models.FaceRectangle) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceRectangle_maxX(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MaxX, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FaceRectangle_maxX(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FaceRectangle", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _FaceRectangle_minY(ctx context.Context, field graphql.CollectedField, obj *models.FaceRectangle) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceRectangle_minY(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MinY, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FaceRectangle_minY(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FaceRectangle", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _FaceRectangle_maxY(ctx context.Context, field graphql.CollectedField, obj *models.FaceRectangle) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_FaceRectangle_maxY(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MaxY, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_FaceRectangle_maxY(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("FaceRectangle", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _ImageFace_id(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageFace_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageFace_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ImageFace", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _ImageFace_media(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageFace_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.ImageFace().Media(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageFace_media(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageFace", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageFace_rectangle(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageFace_rectangle(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Rectangle, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v models.FaceRectangle) graphql.Marshaler { + return ec.marshalNFaceRectangle2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceRectangle(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageFace_rectangle(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageFace", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceRectangle(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageFace_faceGroup(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ImageFace_faceGroup(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.ImageFace().FaceGroup(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.FaceGroup) graphql.Marshaler { + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ImageFace_faceGroup(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageFace", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceGroup(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_id(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Media", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _Media_title(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_title(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Title, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_title(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Media", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Media_path(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_path(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Path, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_path(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Media", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Media_thumbnail(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_thumbnail(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Thumbnail(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.MediaURL) graphql.Marshaler { + return ec.marshalOMediaURL2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Media_thumbnail(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_MediaURL(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_highRes(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_highRes(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().HighRes(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.MediaURL) graphql.Marshaler { + return ec.marshalOMediaURL2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Media_highRes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_MediaURL(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_videoWeb(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_videoWeb(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().VideoWeb(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.MediaURL) graphql.Marshaler { + return ec.marshalOMediaURL2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Media_videoWeb(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_MediaURL(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_album(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_album(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Album(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_album(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_exif(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_exif(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Exif(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.MediaEXIF) graphql.Marshaler { + return ec.marshalOMediaEXIF2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaEXIF(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Media_exif(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_MediaEXIF(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_videoMetadata(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_videoMetadata(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.VideoMetadata, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.VideoMetadata) graphql.Marshaler { + return ec.marshalOVideoMetadata2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Media_videoMetadata(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_VideoMetadata(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_favorite(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_favorite(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Favorite(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_favorite(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Media", field, true, true, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _Media_type(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Type(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v models.MediaType) graphql.Marshaler { + return ec.marshalNMediaType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Media", field, true, true, errors.New("field of type MediaType does not have child fields")) +} + +func (ec *executionContext) _Media_date(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_date(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Date(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v time.Time) graphql.Marshaler { + return ec.marshalNTime2timeᚐTime(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_date(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Media", field, true, false, errors.New("field of type Time does not have child fields")) +} + +func (ec *executionContext) _Media_blurhash(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_blurhash(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Blurhash, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Media_blurhash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Media", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Media_shares(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_shares(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Shares(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_shares(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_downloads(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_downloads(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Downloads(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.MediaDownload) graphql.Marshaler { + return ec.marshalNMediaDownload2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaDownloadᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_downloads(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_MediaDownload(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Media_faces(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Media_faces(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Media().Faces(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.ImageFace) graphql.Marshaler { + return ec.marshalNImageFace2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFaceᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Media_faces(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Media", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ImageFace(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _MediaDownload_title(ctx context.Context, field graphql.CollectedField, obj *models.MediaDownload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaDownload_title(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Title, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaDownload_title(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaDownload", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _MediaDownload_mediaUrl(ctx context.Context, field graphql.CollectedField, obj *models.MediaDownload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaDownload_mediaUrl(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MediaURL, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.MediaURL) graphql.Marshaler { + return ec.marshalNMediaURL2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaDownload_mediaUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MediaDownload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_MediaURL(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _MediaEXIF_id(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_media(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Media(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_media(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MediaEXIF", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _MediaEXIF_description(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_camera(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_camera(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Camera, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_camera(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_maker(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_maker(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Maker, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_maker(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_lens(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_lens(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Lens, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_lens(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_dateShot(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_dateShot(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DateShotWithOffset(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_dateShot(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_exposure(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_exposure(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Exposure, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_exposure(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_aperture(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_aperture(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Aperture, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_aperture(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_iso(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_iso(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Iso, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int64) graphql.Marshaler { + return ec.marshalOInt2ᚖint64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_iso(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_focalLength(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_focalLength(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.FocalLength, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_focalLength(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_flash(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_flash(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Flash, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int64) graphql.Marshaler { + return ec.marshalOInt2ᚖint64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_flash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_exposureProgram(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_exposureProgram(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ExposureProgram, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int64) graphql.Marshaler { + return ec.marshalOInt2ᚖint64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_exposureProgram(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaEXIF", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _MediaEXIF_coordinates(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaEXIF_coordinates(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Coordinates(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Coordinates) graphql.Marshaler { + return ec.marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MediaEXIF_coordinates(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MediaEXIF", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Coordinates(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _MediaURL_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaURL_url(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.URL(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaURL_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaURL", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _MediaURL_width(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaURL_width(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Width, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaURL_width(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaURL", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _MediaURL_height(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaURL_height(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Height, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaURL_height(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaURL", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _MediaURL_fileSize(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MediaURL_fileSize(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.FileSize, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int64) graphql.Marshaler { + return ec.marshalNInt2int64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_MediaURL_fileSize(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MediaURL", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _Mutation_resetAlbumCover(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_resetAlbumCover(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ResetAlbumCover(ctx, fc.Args["albumID"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.Album + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_resetAlbumCover(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_resetAlbumCover_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_setAlbumCover(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_setAlbumCover(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().SetAlbumCover(ctx, fc.Args["coverID"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.Album + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_setAlbumCover(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_setAlbumCover_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_setFaceGroupLabel(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_setFaceGroupLabel(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().SetFaceGroupLabel(ctx, fc.Args["faceGroupID"].(int), fc.Args["label"].(*string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.FaceGroup + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.FaceGroup) graphql.Marshaler { + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_setFaceGroupLabel(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceGroup(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_setFaceGroupLabel_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_combineFaceGroups(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_combineFaceGroups(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().CombineFaceGroups(ctx, fc.Args["destinationFaceGroupID"].(int), fc.Args["sourceFaceGroupIDs"].([]int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.FaceGroup + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.FaceGroup) graphql.Marshaler { + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_combineFaceGroups(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceGroup(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_combineFaceGroups_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_moveImageFaces(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_moveImageFaces(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().MoveImageFaces(ctx, fc.Args["imageFaceIDs"].([]int), fc.Args["destinationFaceGroupID"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.FaceGroup + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.FaceGroup) graphql.Marshaler { + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_moveImageFaces(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceGroup(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_moveImageFaces_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_recognizeUnlabeledFaces(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_recognizeUnlabeledFaces(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Mutation().RecognizeUnlabeledFaces(ctx) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal []*models.ImageFace + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.ImageFace) graphql.Marshaler { + return ec.marshalNImageFace2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFaceᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_recognizeUnlabeledFaces(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ImageFace(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Mutation_detachImageFaces(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_detachImageFaces(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().DetachImageFaces(ctx, fc.Args["imageFaceIDs"].([]int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.FaceGroup + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.FaceGroup) graphql.Marshaler { + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_detachImageFaces(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceGroup(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_detachImageFaces_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_favoriteMedia(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().FavoriteMedia(ctx, fc.Args["mediaId"].(int), fc.Args["favorite"].(bool)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.Media + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_favoriteMedia(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_favoriteMedia_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_scanAll(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_scanAll(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Mutation().ScanAll(ctx) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal *models.ScannerResult + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.ScannerResult) graphql.Marshaler { + return ec.marshalNScannerResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐScannerResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_scanAll(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ScannerResult(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Mutation_scanUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_scanUser(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ScanUser(ctx, fc.Args["userId"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal *models.ScannerResult + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.ScannerResult) graphql.Marshaler { + return ec.marshalNScannerResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐScannerResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_scanUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ScannerResult(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_scanUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_setPeriodicScanInterval(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_setPeriodicScanInterval(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().SetPeriodicScanInterval(ctx, fc.Args["interval"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal int + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_setPeriodicScanInterval(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_setPeriodicScanInterval_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_setScannerConcurrentWorkers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_setScannerConcurrentWorkers(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().SetScannerConcurrentWorkers(ctx, fc.Args["workers"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal int + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_setScannerConcurrentWorkers(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_setScannerConcurrentWorkers_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_shareAlbum(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_shareAlbum(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ShareAlbum(ctx, fc.Args["albumId"].(int), fc.Args["expire"].(*time.Time), fc.Args["password"].(*string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.ShareToken + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_shareAlbum(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_shareAlbum_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_shareMedia(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ShareMedia(ctx, fc.Args["mediaId"].(int), fc.Args["expire"].(*time.Time), fc.Args["password"].(*string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.ShareToken + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_shareMedia(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_shareMedia_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_deleteShareToken(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().DeleteShareToken(ctx, fc.Args["token"].(string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.ShareToken + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_deleteShareToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteShareToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_protectShareToken(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ProtectShareToken(ctx, fc.Args["token"].(string), fc.Args["password"].(*string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.ShareToken + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_protectShareToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_protectShareToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_setExpireShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_setExpireShareToken(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().SetExpireShareToken(ctx, fc.Args["token"].(string), fc.Args["expire"].(*time.Time)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.ShareToken + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_setExpireShareToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_setExpireShareToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_authorizeUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_authorizeUser(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().AuthorizeUser(ctx, fc.Args["username"].(string), fc.Args["password"].(string)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.AuthorizeResult) graphql.Marshaler { + return ec.marshalNAuthorizeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_authorizeUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_AuthorizeResult(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_authorizeUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_initialSetupWizard(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_initialSetupWizard(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().InitialSetupWizard(ctx, fc.Args["username"].(string), fc.Args["password"].(string), fc.Args["rootPath"].(string)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.AuthorizeResult) graphql.Marshaler { + return ec.marshalOAuthorizeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Mutation_initialSetupWizard(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_AuthorizeResult(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_initialSetupWizard_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_updateUser(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().UpdateUser(ctx, fc.Args["id"].(int), fc.Args["username"].(*string), fc.Args["password"].(*string), fc.Args["admin"].(*bool)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal *models.User + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.User) graphql.Marshaler { + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_User(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_createUser(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().CreateUser(ctx, fc.Args["username"].(string), fc.Args["password"].(*string), fc.Args["admin"].(bool)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal *models.User + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.User) graphql.Marshaler { + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_createUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_User(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_deleteUser(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().DeleteUser(ctx, fc.Args["id"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal *models.User + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.User) graphql.Marshaler { + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_deleteUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_User(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_userAddRootPath(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_userAddRootPath(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().UserAddRootPath(ctx, fc.Args["id"].(int), fc.Args["rootPath"].(string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal *models.Album + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Mutation_userAddRootPath(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_userAddRootPath_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_userRemoveRootAlbum(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_userRemoveRootAlbum(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().UserRemoveRootAlbum(ctx, fc.Args["userId"].(int), fc.Args["albumId"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal *models.Album + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Mutation_userRemoveRootAlbum(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_userRemoveRootAlbum_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_changeUserPreferences(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Mutation_changeUserPreferences(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ChangeUserPreferences(ctx, fc.Args["language"].(*string)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.UserPreferences + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.UserPreferences) graphql.Marshaler { + return ec.marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Mutation_changeUserPreferences(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_UserPreferences(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_changeUserPreferences_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Notification_key(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_key(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Key, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Notification_key(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Notification_type(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v models.NotificationType) graphql.Marshaler { + return ec.marshalNNotificationType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotificationType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Notification_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type NotificationType does not have child fields")) +} + +func (ec *executionContext) _Notification_header(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_header(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Header, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Notification_header(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Notification_content(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_content(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Content, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Notification_content(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Notification_progress(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_progress(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Progress, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Notification_progress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _Notification_positive(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_positive(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Positive, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Notification_positive(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _Notification_negative(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_negative(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Negative, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Notification_negative(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _Notification_timeout(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Notification_timeout(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Timeout, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int) graphql.Marshaler { + return ec.marshalOInt2ᚖint(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Notification_timeout(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Notification", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _Query_myAlbums(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_myAlbums(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().MyAlbums(ctx, fc.Args["order"].(*models.Ordering), fc.Args["paginate"].(*models.Pagination), fc.Args["onlyRoot"].(*bool), fc.Args["showEmpty"].(*bool), fc.Args["onlyWithFavorites"].(*bool)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal []*models.Album + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_myAlbums(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_myAlbums_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_album(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_album(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().Album(ctx, fc.Args["id"].(int), fc.Args["tokenCredentials"].(*models.ShareTokenCredentials)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_album(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_album_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_myFaceGroups(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_myFaceGroups(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().MyFaceGroups(ctx, fc.Args["paginate"].(*models.Pagination)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal []*models.FaceGroup + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.FaceGroup) graphql.Marshaler { + return ec.marshalNFaceGroup2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroupᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_myFaceGroups(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceGroup(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_myFaceGroups_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_faceGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_faceGroup(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().FaceGroup(ctx, fc.Args["id"].(int)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.FaceGroup + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.FaceGroup) graphql.Marshaler { + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_faceGroup(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_FaceGroup(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_faceGroup_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_myMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_myMedia(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().MyMedia(ctx, fc.Args["order"].(*models.Ordering), fc.Args["paginate"].(*models.Pagination)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal []*models.Media + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_myMedia(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_myMedia_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_media(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().Media(ctx, fc.Args["id"].(int), fc.Args["tokenCredentials"].(*models.ShareTokenCredentials)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_media(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_media_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_mediaList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_mediaList(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().MediaList(ctx, fc.Args["ids"].([]int)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_mediaList(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_mediaList_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_myMediaGeoJson(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_myMediaGeoJson(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().MyMediaGeoJSON(ctx) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal any + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v any) graphql.Marshaler { + return ec.marshalNAny2interface(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_myMediaGeoJson(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Query", field, true, true, errors.New("field of type Any does not have child fields")) +} + +func (ec *executionContext) _Query_mapboxToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_mapboxToken(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().MapboxToken(ctx) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query_mapboxToken(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Query", field, true, true, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _Query_search(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_search(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().Search(ctx, fc.Args["query"].(string), fc.Args["limitMedia"].(*int), fc.Args["limitAlbums"].(*int)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.SearchResult) graphql.Marshaler { + return ec.marshalNSearchResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSearchResult(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_search(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_SearchResult(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_search_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_shareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_shareToken(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().ShareToken(ctx, fc.Args["credentials"].(models.ShareTokenCredentials)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.ShareToken) graphql.Marshaler { + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_shareToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_ShareToken(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_shareToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_shareTokenValidatePassword(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_shareTokenValidatePassword(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().ShareTokenValidatePassword(ctx, fc.Args["credentials"].(models.ShareTokenCredentials)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_shareTokenValidatePassword(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_shareTokenValidatePassword_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_siteInfo(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_siteInfo(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().SiteInfo(ctx) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.SiteInfo) graphql.Marshaler { + return ec.marshalNSiteInfo2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSiteInfo(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_siteInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_SiteInfo(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_myTimeline(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_myTimeline(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().MyTimeline(ctx, fc.Args["paginate"].(*models.Pagination), fc.Args["onlyFavorites"].(*bool), fc.Args["fromDate"].(*time.Time)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal []*models.Media + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_myTimeline(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_myTimeline_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_user(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Query().User(ctx, fc.Args["order"].(*models.Ordering), fc.Args["paginate"].(*models.Pagination)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal []*models.User + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.User) graphql.Marshaler { + return ec.marshalNUser2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_user(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_User(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_user_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_myUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_myUser(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().MyUser(ctx) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.User + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.User) graphql.Marshaler { + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_myUser(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_User(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_myUserPreferences(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_myUserPreferences(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().MyUserPreferences(ctx) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAuthorized == nil { + var zeroVal *models.UserPreferences + return zeroVal, errors.New("directive isAuthorized is not implemented") + } + return ec.Directives.IsAuthorized(ctx, nil, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v *models.UserPreferences) graphql.Marshaler { + return ec.marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_myUserPreferences(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_UserPreferences(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query___type(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.IntrospectType(fc.Args["name"].(string)) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query___schema(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.IntrospectSchema() + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { + return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Schema(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _ScannerResult_finished(ctx context.Context, field graphql.CollectedField, obj *models.ScannerResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ScannerResult_finished(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Finished, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ScannerResult_finished(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ScannerResult", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _ScannerResult_success(ctx context.Context, field graphql.CollectedField, obj *models.ScannerResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ScannerResult_success(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Success, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ScannerResult_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ScannerResult", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _ScannerResult_progress(ctx context.Context, field graphql.CollectedField, obj *models.ScannerResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ScannerResult_progress(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Progress, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ScannerResult_progress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ScannerResult", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _ScannerResult_message(ctx context.Context, field graphql.CollectedField, obj *models.ScannerResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ScannerResult_message(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Message, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ScannerResult_message(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ScannerResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _SearchResult_query(ctx context.Context, field graphql.CollectedField, obj *models.SearchResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_SearchResult_query(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Query, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_SearchResult_query(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("SearchResult", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _SearchResult_albums(ctx context.Context, field graphql.CollectedField, obj *models.SearchResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_SearchResult_albums(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Albums, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_SearchResult_albums(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SearchResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _SearchResult_media(ctx context.Context, field graphql.CollectedField, obj *models.SearchResult) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_SearchResult_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Media, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_SearchResult_media(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SearchResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _ShareToken_id(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ShareToken_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ShareToken_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ShareToken", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _ShareToken_token(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ShareToken_token(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Token(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ShareToken_token(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ShareToken", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _ShareToken_owner(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ShareToken_owner(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Owner, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v models.User) graphql.Marshaler { + return ec.marshalNUser2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ShareToken_owner(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ShareToken", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_User(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _ShareToken_expire(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ShareToken_expire(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Expire, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *time.Time) graphql.Marshaler { + return ec.marshalOTime2ᚖtimeᚐTime(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ShareToken_expire(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ShareToken", field, false, false, errors.New("field of type Time does not have child fields")) +} + +func (ec *executionContext) _ShareToken_hasPassword(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ShareToken_hasPassword(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.ShareToken().HasPassword(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_ShareToken_hasPassword(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("ShareToken", field, true, true, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _ShareToken_album(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ShareToken_album(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Album, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ShareToken_album(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ShareToken", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _ShareToken_media(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_ShareToken_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Media, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Media) graphql.Marshaler { + return ec.marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_ShareToken_media(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ShareToken", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _SiteInfo_initialSetup(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_SiteInfo_initialSetup(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.InitialSetup, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_SiteInfo_initialSetup(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("SiteInfo", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _SiteInfo_faceDetectionEnabled(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_SiteInfo_faceDetectionEnabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.SiteInfo().FaceDetectionEnabled(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_SiteInfo_faceDetectionEnabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("SiteInfo", field, true, true, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _SiteInfo_periodicScanInterval(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_SiteInfo_periodicScanInterval(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PeriodicScanInterval, nil + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal int + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, obj, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_SiteInfo_periodicScanInterval(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("SiteInfo", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _SiteInfo_concurrentWorkers(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_SiteInfo_concurrentWorkers(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ConcurrentWorkers, nil + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal int + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, obj, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_SiteInfo_concurrentWorkers(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("SiteInfo", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _Subscription_notification(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) { + return graphql.ResolveFieldStream( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Subscription_notification(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Subscription().Notification(ctx) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Notification) graphql.Marshaler { + return ec.marshalNNotification2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotification(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Subscription_notification(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Subscription", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Notification(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _TimelineGroup_album(ctx context.Context, field graphql.CollectedField, obj *models.TimelineGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_TimelineGroup_album(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Album, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_TimelineGroup_album(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TimelineGroup", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _TimelineGroup_media(ctx context.Context, field graphql.CollectedField, obj *models.TimelineGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_TimelineGroup_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Media, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_TimelineGroup_media(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TimelineGroup", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _TimelineGroup_mediaTotal(ctx context.Context, field graphql.CollectedField, obj *models.TimelineGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_TimelineGroup_mediaTotal(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MediaTotal, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_TimelineGroup_mediaTotal(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("TimelineGroup", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _TimelineGroup_date(ctx context.Context, field graphql.CollectedField, obj *models.TimelineGroup) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_TimelineGroup_date(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Date, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v time.Time) graphql.Marshaler { + return ec.marshalNTime2timeᚐTime(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_TimelineGroup_date(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("TimelineGroup", field, false, false, errors.New("field of type Time does not have child fields")) +} + +func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_User_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_User_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("User", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_User_username(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Username, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_User_username(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("User", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _User_albums(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_User_albums(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.User().Albums(ctx, obj) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal []*models.Album + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, obj, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_User_albums(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _User_rootAlbums(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_User_rootAlbums(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.User().RootAlbums(ctx, obj) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + if ec.Directives.IsAdmin == nil { + var zeroVal []*models.Album + return zeroVal, errors.New("directive isAdmin is not implemented") + } + return ec.Directives.IsAdmin(ctx, obj, directive0) + } + + next = directive1 + return next + }, + func(ctx context.Context, selections ast.SelectionSet, v []*models.Album) graphql.Marshaler { + return ec.marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_User_rootAlbums(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Album(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _User_admin(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_User_admin(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Admin, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_User_admin(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("User", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _UserPreferences_id(ctx context.Context, field graphql.CollectedField, obj *models.UserPreferences) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_UserPreferences_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_UserPreferences_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("UserPreferences", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _UserPreferences_language(ctx context.Context, field graphql.CollectedField, obj *models.UserPreferences) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_UserPreferences_language(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Language, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.LanguageTranslation) graphql.Marshaler { + return ec.marshalOLanguageTranslation2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐLanguageTranslation(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_UserPreferences_language(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("UserPreferences", field, false, false, errors.New("field of type LanguageTranslation does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_id(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_id(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNID2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type ID does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_media(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_media(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Media(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *models.Media) graphql.Marshaler { + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_media(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VideoMetadata", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_Media(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _VideoMetadata_width(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_width(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Width, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_width(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_height(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_height(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Height, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_height(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_duration(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_duration(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Duration, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_duration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_codec(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_codec(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Codec, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_codec(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_framerate(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_framerate(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Framerate, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { + return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_framerate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type Float does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_bitrate(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_bitrate(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Bitrate, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_bitrate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_colorProfile(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_colorProfile(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ColorProfile, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_colorProfile(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _VideoMetadata_audio(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_VideoMetadata_audio(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Audio, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_VideoMetadata_audio(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("VideoMetadata", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Directive_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_isRepeatable(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsRepeatable, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_isRepeatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_locations(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Locations, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []string) graphql.Marshaler { + return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_locations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Directive", field, false, false, errors.New("field of type __DirectiveLocation does not have child fields")) +} + +func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Directive_args(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Args, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Directive_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Directive_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___EnumValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___EnumValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___EnumValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___EnumValue_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___EnumValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__EnumValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Field_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_args(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Args, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Field_args_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Field_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Field_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Field_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Field", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_type(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Type, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__InputValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_defaultValue(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DefaultValue, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_defaultValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___InputValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_isDeprecated(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsDeprecated(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___InputValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) ___InputValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___InputValue_deprecationReason(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.DeprecationReason(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___InputValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__InputValue", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Schema_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Schema", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_types(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Types(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_types(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_queryType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.QueryType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_queryType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_mutationType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.MutationType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_mutationType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_subscriptionType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SubscriptionType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Schema_subscriptionType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Schema_directives(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Directives(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { + return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Schema_directives(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Directive(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_kind(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Kind(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalN__TypeKind2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext___Type_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type __TypeKind does not have child fields")) +} + +func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_description(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Description(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_specifiedByURL(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.SpecifiedByURL(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_fields(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return obj.Fields(fc.Args["includeDeprecated"].(bool)), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Field(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_fields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_interfaces(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Interfaces(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_interfaces(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_possibleTypes(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PossibleTypes(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_possibleTypes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_enumValues(ctx, field) + }, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return obj.EnumValues(fc.Args["includeDeprecated"].(bool)), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___EnumValue(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_enumValues_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_inputFields(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.InputFields(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_inputFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___InputValue(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_ofType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.OfType(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_ofType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields___Type(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_isOneOf(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext___Type_isOneOf(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IsOneOf(), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalOBoolean2bool(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("__Type", field, true, false, errors.New("field of type Boolean does not have child fields")) +} + +// endregion **************************** field.gotpl ***************************** + +// region **************************** input.gotpl ***************************** + +func (ec *executionContext) unmarshalInputOrdering(ctx context.Context, obj any) (models.Ordering, error) { + var it models.Ordering + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"order_by", "order_direction"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "order_by": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("order_by")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.OrderBy = data + case "order_direction": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("order_direction")) + data, err := ec.unmarshalOOrderDirection2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrderDirection(ctx, v) + if err != nil { + return it, err + } + it.OrderDirection = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputPagination(ctx context.Context, obj any) (models.Pagination, error) { + var it models.Pagination + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"limit", "offset"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "limit": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.Limit = data + case "offset": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("offset")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.Offset = data + } + } + return it, nil +} + +func (ec *executionContext) unmarshalInputShareTokenCredentials(ctx context.Context, obj any) (models.ShareTokenCredentials, error) { + var it models.ShareTokenCredentials + if obj == nil { + return it, nil + } + + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"token", "password"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "token": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Token = data + case "password": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Password = data + } + } + return it, nil +} + +// endregion **************************** input.gotpl ***************************** + +// region ************************** interface.gotpl *************************** + +// endregion ************************** interface.gotpl *************************** + +// region **************************** object.gotpl **************************** + +var albumImplementors = []string{"Album"} + +func (ec *executionContext) _Album(ctx context.Context, sel ast.SelectionSet, obj *models.Album) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, albumImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Album") + case "id": + out.Values[i] = ec._Album_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "title": + out.Values[i] = ec._Album_title(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "media": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Album_media(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "subAlbums": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Album_subAlbums(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "parentAlbum": + out.Values[i] = ec._Album_parentAlbum(ctx, field, obj) + case "owner": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Album_owner(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "filePath": + out.Values[i] = ec._Album_filePath(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "thumbnail": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Album_thumbnail(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "path": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Album_path(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "shares": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Album_shares(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var authorizeResultImplementors = []string{"AuthorizeResult"} + +func (ec *executionContext) _AuthorizeResult(ctx context.Context, sel ast.SelectionSet, obj *models.AuthorizeResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, authorizeResultImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AuthorizeResult") + case "success": + out.Values[i] = ec._AuthorizeResult_success(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "status": + out.Values[i] = ec._AuthorizeResult_status(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "token": + out.Values[i] = ec._AuthorizeResult_token(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var coordinatesImplementors = []string{"Coordinates"} + +func (ec *executionContext) _Coordinates(ctx context.Context, sel ast.SelectionSet, obj *models.Coordinates) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, coordinatesImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Coordinates") + case "latitude": + out.Values[i] = ec._Coordinates_latitude(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "longitude": + out.Values[i] = ec._Coordinates_longitude(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var faceGroupImplementors = []string{"FaceGroup"} + +func (ec *executionContext) _FaceGroup(ctx context.Context, sel ast.SelectionSet, obj *models.FaceGroup) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, faceGroupImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("FaceGroup") + case "id": + out.Values[i] = ec._FaceGroup_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "label": + out.Values[i] = ec._FaceGroup_label(ctx, field, obj) + case "imageFaces": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._FaceGroup_imageFaces(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "imageFaceCount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._FaceGroup_imageFaceCount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var faceRectangleImplementors = []string{"FaceRectangle"} + +func (ec *executionContext) _FaceRectangle(ctx context.Context, sel ast.SelectionSet, obj *models.FaceRectangle) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, faceRectangleImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("FaceRectangle") + case "minX": + out.Values[i] = ec._FaceRectangle_minX(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "maxX": + out.Values[i] = ec._FaceRectangle_maxX(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "minY": + out.Values[i] = ec._FaceRectangle_minY(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "maxY": + out.Values[i] = ec._FaceRectangle_maxY(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var imageFaceImplementors = []string{"ImageFace"} + +func (ec *executionContext) _ImageFace(ctx context.Context, sel ast.SelectionSet, obj *models.ImageFace) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, imageFaceImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ImageFace") + case "id": + out.Values[i] = ec._ImageFace_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "media": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ImageFace_media(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "rectangle": + out.Values[i] = ec._ImageFace_rectangle(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "faceGroup": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ImageFace_faceGroup(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var mediaImplementors = []string{"Media"} + +func (ec *executionContext) _Media(ctx context.Context, sel ast.SelectionSet, obj *models.Media) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mediaImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Media") + case "id": + out.Values[i] = ec._Media_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "title": + out.Values[i] = ec._Media_title(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "path": + out.Values[i] = ec._Media_path(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "thumbnail": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_thumbnail(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "highRes": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_highRes(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "videoWeb": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_videoWeb(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "album": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_album(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "exif": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_exif(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "videoMetadata": + out.Values[i] = ec._Media_videoMetadata(ctx, field, obj) + case "favorite": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_favorite(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "type": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_type(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "date": + out.Values[i] = ec._Media_date(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "blurhash": + out.Values[i] = ec._Media_blurhash(ctx, field, obj) + case "shares": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_shares(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "downloads": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_downloads(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "faces": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Media_faces(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var mediaDownloadImplementors = []string{"MediaDownload"} + +func (ec *executionContext) _MediaDownload(ctx context.Context, sel ast.SelectionSet, obj *models.MediaDownload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mediaDownloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MediaDownload") + case "title": + out.Values[i] = ec._MediaDownload_title(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "mediaUrl": + out.Values[i] = ec._MediaDownload_mediaUrl(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var mediaEXIFImplementors = []string{"MediaEXIF"} + +func (ec *executionContext) _MediaEXIF(ctx context.Context, sel ast.SelectionSet, obj *models.MediaEXIF) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mediaEXIFImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MediaEXIF") + case "id": + out.Values[i] = ec._MediaEXIF_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "media": + out.Values[i] = ec._MediaEXIF_media(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec._MediaEXIF_description(ctx, field, obj) + case "camera": + out.Values[i] = ec._MediaEXIF_camera(ctx, field, obj) + case "maker": + out.Values[i] = ec._MediaEXIF_maker(ctx, field, obj) + case "lens": + out.Values[i] = ec._MediaEXIF_lens(ctx, field, obj) + case "dateShot": + out.Values[i] = ec._MediaEXIF_dateShot(ctx, field, obj) + case "exposure": + out.Values[i] = ec._MediaEXIF_exposure(ctx, field, obj) + case "aperture": + out.Values[i] = ec._MediaEXIF_aperture(ctx, field, obj) + case "iso": + out.Values[i] = ec._MediaEXIF_iso(ctx, field, obj) + case "focalLength": + out.Values[i] = ec._MediaEXIF_focalLength(ctx, field, obj) + case "flash": + out.Values[i] = ec._MediaEXIF_flash(ctx, field, obj) + case "exposureProgram": + out.Values[i] = ec._MediaEXIF_exposureProgram(ctx, field, obj) + case "coordinates": + out.Values[i] = ec._MediaEXIF_coordinates(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var mediaURLImplementors = []string{"MediaURL"} + +func (ec *executionContext) _MediaURL(ctx context.Context, sel ast.SelectionSet, obj *models.MediaURL) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mediaURLImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MediaURL") + case "url": + out.Values[i] = ec._MediaURL_url(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "width": + out.Values[i] = ec._MediaURL_width(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "height": + out.Values[i] = ec._MediaURL_height(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fileSize": + out.Values[i] = ec._MediaURL_fileSize(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var mutationImplementors = []string{"Mutation"} + +func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Mutation", + }) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Mutation") + case "resetAlbumCover": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_resetAlbumCover(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "setAlbumCover": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_setAlbumCover(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "setFaceGroupLabel": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_setFaceGroupLabel(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "combineFaceGroups": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_combineFaceGroups(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "moveImageFaces": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_moveImageFaces(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "recognizeUnlabeledFaces": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_recognizeUnlabeledFaces(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "detachImageFaces": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_detachImageFaces(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "favoriteMedia": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_favoriteMedia(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "scanAll": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_scanAll(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "scanUser": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_scanUser(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "setPeriodicScanInterval": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_setPeriodicScanInterval(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "setScannerConcurrentWorkers": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_setScannerConcurrentWorkers(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "shareAlbum": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_shareAlbum(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "shareMedia": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_shareMedia(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteShareToken": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteShareToken(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "protectShareToken": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_protectShareToken(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "setExpireShareToken": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_setExpireShareToken(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "authorizeUser": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_authorizeUser(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "initialSetupWizard": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_initialSetupWizard(ctx, field) + }) + case "updateUser": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateUser(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createUser": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createUser(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteUser": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteUser(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "userAddRootPath": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_userAddRootPath(ctx, field) + }) + case "userRemoveRootAlbum": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_userRemoveRootAlbum(ctx, field) + }) + case "changeUserPreferences": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_changeUserPreferences(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var notificationImplementors = []string{"Notification"} + +func (ec *executionContext) _Notification(ctx context.Context, sel ast.SelectionSet, obj *models.Notification) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, notificationImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Notification") + case "key": + out.Values[i] = ec._Notification_key(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "type": + out.Values[i] = ec._Notification_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "header": + out.Values[i] = ec._Notification_header(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "content": + out.Values[i] = ec._Notification_content(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "progress": + out.Values[i] = ec._Notification_progress(ctx, field, obj) + case "positive": + out.Values[i] = ec._Notification_positive(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "negative": + out.Values[i] = ec._Notification_negative(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "timeout": + out.Values[i] = ec._Notification_timeout(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var queryImplementors = []string{"Query"} + +func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Query", + }) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Query") + case "myAlbums": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myAlbums(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "album": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_album(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "myFaceGroups": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myFaceGroups(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "faceGroup": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_faceGroup(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "myMedia": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myMedia(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "media": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_media(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "mediaList": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_mediaList(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "myMediaGeoJson": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myMediaGeoJson(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "mapboxToken": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_mapboxToken(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "search": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_search(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "shareToken": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_shareToken(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "shareTokenValidatePassword": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_shareTokenValidatePassword(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "siteInfo": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_siteInfo(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "myTimeline": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myTimeline(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "user": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_user(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "myUser": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myUser(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "myUserPreferences": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myUserPreferences(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "__type": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___type(ctx, field) + }) + case "__schema": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___schema(ctx, field) + }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var scannerResultImplementors = []string{"ScannerResult"} + +func (ec *executionContext) _ScannerResult(ctx context.Context, sel ast.SelectionSet, obj *models.ScannerResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, scannerResultImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ScannerResult") + case "finished": + out.Values[i] = ec._ScannerResult_finished(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "success": + out.Values[i] = ec._ScannerResult_success(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "progress": + out.Values[i] = ec._ScannerResult_progress(ctx, field, obj) + case "message": + out.Values[i] = ec._ScannerResult_message(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var searchResultImplementors = []string{"SearchResult"} + +func (ec *executionContext) _SearchResult(ctx context.Context, sel ast.SelectionSet, obj *models.SearchResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, searchResultImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("SearchResult") + case "query": + out.Values[i] = ec._SearchResult_query(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "albums": + out.Values[i] = ec._SearchResult_albums(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "media": + out.Values[i] = ec._SearchResult_media(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var shareTokenImplementors = []string{"ShareToken"} + +func (ec *executionContext) _ShareToken(ctx context.Context, sel ast.SelectionSet, obj *models.ShareToken) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, shareTokenImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ShareToken") + case "id": + out.Values[i] = ec._ShareToken_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "token": + out.Values[i] = ec._ShareToken_token(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "owner": + out.Values[i] = ec._ShareToken_owner(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "expire": + out.Values[i] = ec._ShareToken_expire(ctx, field, obj) + case "hasPassword": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ShareToken_hasPassword(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "album": + out.Values[i] = ec._ShareToken_album(ctx, field, obj) + case "media": + out.Values[i] = ec._ShareToken_media(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var siteInfoImplementors = []string{"SiteInfo"} + +func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet, obj *models.SiteInfo) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, siteInfoImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("SiteInfo") + case "initialSetup": + out.Values[i] = ec._SiteInfo_initialSetup(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "faceDetectionEnabled": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._SiteInfo_faceDetectionEnabled(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "periodicScanInterval": + out.Values[i] = ec._SiteInfo_periodicScanInterval(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "concurrentWorkers": + out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var subscriptionImplementors = []string{"Subscription"} + +func (ec *executionContext) _Subscription(ctx context.Context, sel ast.SelectionSet) func(ctx context.Context) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, subscriptionImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Subscription", + }) + if len(fields) != 1 { + graphql.AddErrorf(ctx, "must subscribe to exactly one stream") + return nil + } + + switch fields[0].Name { + case "notification": + return ec._Subscription_notification(ctx, fields[0]) + default: + panic("unknown field " + strconv.Quote(fields[0].Name)) + } +} + +var timelineGroupImplementors = []string{"TimelineGroup"} + +func (ec *executionContext) _TimelineGroup(ctx context.Context, sel ast.SelectionSet, obj *models.TimelineGroup) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, timelineGroupImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("TimelineGroup") + case "album": + out.Values[i] = ec._TimelineGroup_album(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "media": + out.Values[i] = ec._TimelineGroup_media(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "mediaTotal": + out.Values[i] = ec._TimelineGroup_mediaTotal(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "date": + out.Values[i] = ec._TimelineGroup_date(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var userImplementors = []string{"User"} + +func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *models.User) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("User") + case "id": + out.Values[i] = ec._User_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "username": + out.Values[i] = ec._User_username(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "albums": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._User_albums(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "rootAlbums": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._User_rootAlbums(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "admin": + out.Values[i] = ec._User_admin(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var userPreferencesImplementors = []string{"UserPreferences"} + +func (ec *executionContext) _UserPreferences(ctx context.Context, sel ast.SelectionSet, obj *models.UserPreferences) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, userPreferencesImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("UserPreferences") + case "id": + out.Values[i] = ec._UserPreferences_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "language": + out.Values[i] = ec._UserPreferences_language(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var videoMetadataImplementors = []string{"VideoMetadata"} + +func (ec *executionContext) _VideoMetadata(ctx context.Context, sel ast.SelectionSet, obj *models.VideoMetadata) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, videoMetadataImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("VideoMetadata") + case "id": + out.Values[i] = ec._VideoMetadata_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "media": + out.Values[i] = ec._VideoMetadata_media(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "width": + out.Values[i] = ec._VideoMetadata_width(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "height": + out.Values[i] = ec._VideoMetadata_height(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "duration": + out.Values[i] = ec._VideoMetadata_duration(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "codec": + out.Values[i] = ec._VideoMetadata_codec(ctx, field, obj) + case "framerate": + out.Values[i] = ec._VideoMetadata_framerate(ctx, field, obj) + case "bitrate": + out.Values[i] = ec._VideoMetadata_bitrate(ctx, field, obj) + case "colorProfile": + out.Values[i] = ec._VideoMetadata_colorProfile(ctx, field, obj) + case "audio": + out.Values[i] = ec._VideoMetadata_audio(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __DirectiveImplementors = []string{"__Directive"} + +func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __DirectiveImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Directive") + case "name": + out.Values[i] = ec.___Directive_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___Directive_description(ctx, field, obj) + case "isRepeatable": + out.Values[i] = ec.___Directive_isRepeatable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "locations": + out.Values[i] = ec.___Directive_locations(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "args": + out.Values[i] = ec.___Directive_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __EnumValueImplementors = []string{"__EnumValue"} + +func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __EnumValueImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__EnumValue") + case "name": + out.Values[i] = ec.___EnumValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___EnumValue_description(ctx, field, obj) + case "isDeprecated": + out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __FieldImplementors = []string{"__Field"} + +func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __FieldImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Field") + case "name": + out.Values[i] = ec.___Field_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___Field_description(ctx, field, obj) + case "args": + out.Values[i] = ec.___Field_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "type": + out.Values[i] = ec.___Field_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "isDeprecated": + out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __InputValueImplementors = []string{"__InputValue"} + +func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __InputValueImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__InputValue") + case "name": + out.Values[i] = ec.___InputValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___InputValue_description(ctx, field, obj) + case "type": + out.Values[i] = ec.___InputValue_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "defaultValue": + out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj) + case "isDeprecated": + out.Values[i] = ec.___InputValue_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___InputValue_deprecationReason(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __SchemaImplementors = []string{"__Schema"} + +func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __SchemaImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Schema") + case "description": + out.Values[i] = ec.___Schema_description(ctx, field, obj) + case "types": + out.Values[i] = ec.___Schema_types(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "queryType": + out.Values[i] = ec.___Schema_queryType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "mutationType": + out.Values[i] = ec.___Schema_mutationType(ctx, field, obj) + case "subscriptionType": + out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj) + case "directives": + out.Values[i] = ec.___Schema_directives(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __TypeImplementors = []string{"__Type"} + +func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __TypeImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Type") + case "kind": + out.Values[i] = ec.___Type_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "name": + out.Values[i] = ec.___Type_name(ctx, field, obj) + case "description": + out.Values[i] = ec.___Type_description(ctx, field, obj) + case "specifiedByURL": + out.Values[i] = ec.___Type_specifiedByURL(ctx, field, obj) + case "fields": + out.Values[i] = ec.___Type_fields(ctx, field, obj) + case "interfaces": + out.Values[i] = ec.___Type_interfaces(ctx, field, obj) + case "possibleTypes": + out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj) + case "enumValues": + out.Values[i] = ec.___Type_enumValues(ctx, field, obj) + case "inputFields": + out.Values[i] = ec.___Type_inputFields(ctx, field, obj) + case "ofType": + out.Values[i] = ec.___Type_ofType(ctx, field, obj) + case "isOneOf": + out.Values[i] = ec.___Type_isOneOf(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +// endregion **************************** object.gotpl **************************** + +// region ***************************** type.gotpl ***************************** + +func (ec *executionContext) marshalNAlbum2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx context.Context, sel ast.SelectionSet, v models.Album) graphql.Marshaler { + return ec._Album(ctx, sel, &v) +} + +func (ec *executionContext) marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.Album) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx context.Context, sel ast.SelectionSet, v *models.Album) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Album(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNAny2interface(ctx context.Context, v any) (any, error) { + res, err := graphql.UnmarshalAny(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNAny2interface(ctx context.Context, sel ast.SelectionSet, v any) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + _ = sel + res := graphql.MarshalAny(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNAuthorizeResult2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx context.Context, sel ast.SelectionSet, v models.AuthorizeResult) graphql.Marshaler { + return ec._AuthorizeResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNAuthorizeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx context.Context, sel ast.SelectionSet, v *models.AuthorizeResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._AuthorizeResult(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { + res, err := graphql.UnmarshalBoolean(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + _ = sel + res := graphql.MarshalBoolean(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNFaceGroup2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx context.Context, sel ast.SelectionSet, v models.FaceGroup) graphql.Marshaler { + return ec._FaceGroup(ctx, sel, &v) +} + +func (ec *executionContext) marshalNFaceGroup2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroupᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.FaceGroup) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx context.Context, sel ast.SelectionSet, v *models.FaceGroup) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._FaceGroup(ctx, sel, v) +} + +func (ec *executionContext) marshalNFaceRectangle2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceRectangle(ctx context.Context, sel ast.SelectionSet, v models.FaceRectangle) graphql.Marshaler { + return ec._FaceRectangle(ctx, sel, &v) +} + +func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v any) (float64, error) { + res, err := graphql.UnmarshalFloatContext(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.SelectionSet, v float64) graphql.Marshaler { + _ = sel + res := graphql.MarshalFloatContext(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return graphql.WrapContextMarshaler(ctx, res) +} + +func (ec *executionContext) unmarshalNID2int(ctx context.Context, v any) (int, error) { + res, err := graphql.UnmarshalIntID(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNID2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + _ = sel + res := graphql.MarshalIntID(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNID2ᚕintᚄ(ctx context.Context, v any) ([]int, error) { + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]int, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNID2int(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNID2ᚕintᚄ(ctx context.Context, sel ast.SelectionSet, v []int) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNID2int(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNImageFace2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFaceᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.ImageFace) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNImageFace2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFace(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNImageFace2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFace(ctx context.Context, sel ast.SelectionSet, v *models.ImageFace) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ImageFace(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) { + res, err := graphql.UnmarshalInt(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + _ = sel + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNInt2int64(ctx context.Context, v any) (int64, error) { + res, err := graphql.UnmarshalInt64(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int64(ctx context.Context, sel ast.SelectionSet, v int64) graphql.Marshaler { + _ = sel + res := graphql.MarshalInt64(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNMedia2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v models.Media) graphql.Marshaler { + return ec._Media(ctx, sel, &v) +} + +func (ec *executionContext) marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.Media) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v *models.Media) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Media(ctx, sel, v) +} + +func (ec *executionContext) marshalNMediaDownload2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaDownloadᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.MediaDownload) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNMediaDownload2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaDownload(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNMediaDownload2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaDownload(ctx context.Context, sel ast.SelectionSet, v *models.MediaDownload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._MediaDownload(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNMediaType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaType(ctx context.Context, v any) (models.MediaType, error) { + tmp, err := graphql.UnmarshalString(v) + res := models.MediaType(tmp) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNMediaType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaType(ctx context.Context, sel ast.SelectionSet, v models.MediaType) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(string(v)) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNMediaURL2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx context.Context, sel ast.SelectionSet, v *models.MediaURL) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._MediaURL(ctx, sel, v) +} + +func (ec *executionContext) marshalNNotification2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotification(ctx context.Context, sel ast.SelectionSet, v models.Notification) graphql.Marshaler { + return ec._Notification(ctx, sel, &v) +} + +func (ec *executionContext) marshalNNotification2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotification(ctx context.Context, sel ast.SelectionSet, v *models.Notification) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Notification(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNNotificationType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotificationType(ctx context.Context, v any) (models.NotificationType, error) { + var res models.NotificationType + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNNotificationType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐNotificationType(ctx context.Context, sel ast.SelectionSet, v models.NotificationType) graphql.Marshaler { + return v +} + +func (ec *executionContext) marshalNScannerResult2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐScannerResult(ctx context.Context, sel ast.SelectionSet, v models.ScannerResult) graphql.Marshaler { + return ec._ScannerResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNScannerResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐScannerResult(ctx context.Context, sel ast.SelectionSet, v *models.ScannerResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ScannerResult(ctx, sel, v) +} + +func (ec *executionContext) marshalNSearchResult2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSearchResult(ctx context.Context, sel ast.SelectionSet, v models.SearchResult) graphql.Marshaler { + return ec._SearchResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNSearchResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSearchResult(ctx context.Context, sel ast.SelectionSet, v *models.SearchResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._SearchResult(ctx, sel, v) +} + +func (ec *executionContext) marshalNShareToken2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx context.Context, sel ast.SelectionSet, v models.ShareToken) graphql.Marshaler { + return ec._ShareToken(ctx, sel, &v) +} + +func (ec *executionContext) marshalNShareToken2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.ShareToken) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx context.Context, sel ast.SelectionSet, v *models.ShareToken) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ShareToken(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNShareTokenCredentials2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx context.Context, v any) (models.ShareTokenCredentials, error) { + res, err := ec.unmarshalInputShareTokenCredentials(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNSiteInfo2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSiteInfo(ctx context.Context, sel ast.SelectionSet, v models.SiteInfo) graphql.Marshaler { + return ec._SiteInfo(ctx, sel, &v) +} + +func (ec *executionContext) marshalNSiteInfo2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSiteInfo(ctx context.Context, sel ast.SelectionSet, v *models.SiteInfo) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._SiteInfo(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNTime2timeᚐTime(ctx context.Context, v any) (time.Time, error) { + res, err := graphql.UnmarshalTime(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNTime2timeᚐTime(ctx context.Context, sel ast.SelectionSet, v time.Time) graphql.Marshaler { + _ = sel + res := graphql.MarshalTime(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNUser2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v models.User) graphql.Marshaler { + return ec._User(ctx, sel, &v) +} + +func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.User) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v *models.User) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._User(ctx, sel, v) +} + +func (ec *executionContext) marshalNUserPreferences2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx context.Context, sel ast.SelectionSet, v models.UserPreferences) graphql.Marshaler { + return ec._UserPreferences(ctx, sel, &v) +} + +func (ec *executionContext) marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx context.Context, sel ast.SelectionSet, v *models.UserPreferences) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._UserPreferences(ctx, sel, v) +} + +func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { + return ec.___Directive(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalN__DirectiveLocation2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalN__DirectiveLocation2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__DirectiveLocation2string(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v introspection.EnumValue) graphql.Marshaler { + return ec.___EnumValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v introspection.Field) graphql.Marshaler { + return ec.___Field(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v introspection.InputValue) graphql.Marshaler { + return ec.___InputValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v introspection.Type) graphql.Marshaler { + return ec.___Type(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +func (ec *executionContext) unmarshalN__TypeKind2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx context.Context, sel ast.SelectionSet, v *models.Album) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Album(ctx, sel, v) +} + +func (ec *executionContext) marshalOAuthorizeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx context.Context, sel ast.SelectionSet, v *models.AuthorizeResult) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._AuthorizeResult(ctx, sel, v) +} + +func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v any) (bool, error) { + res, err := graphql.UnmarshalBoolean(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + _ = sel + _ = ctx + res := graphql.MarshalBoolean(v) + return res +} + +func (ec *executionContext) unmarshalOBoolean2ᚖbool(ctx context.Context, v any) (*bool, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalBoolean(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast.SelectionSet, v *bool) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalBoolean(*v) + return res +} + +func (ec *executionContext) marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx context.Context, sel ast.SelectionSet, v *models.Coordinates) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Coordinates(ctx, sel, v) +} + +func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v any) (*float64, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalFloatContext(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOFloat2ᚖfloat64(ctx context.Context, sel ast.SelectionSet, v *float64) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + res := graphql.MarshalFloatContext(*v) + return graphql.WrapContextMarshaler(ctx, res) +} + +func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalInt(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.SelectionSet, v *int) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalInt(*v) + return res +} + +func (ec *executionContext) unmarshalOInt2ᚖint64(ctx context.Context, v any) (*int64, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalInt64(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOInt2ᚖint64(ctx context.Context, sel ast.SelectionSet, v *int64) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalInt64(*v) + return res +} + +func (ec *executionContext) unmarshalOLanguageTranslation2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐLanguageTranslation(ctx context.Context, v any) (*models.LanguageTranslation, error) { + if v == nil { + return nil, nil + } + var res = new(models.LanguageTranslation) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOLanguageTranslation2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐLanguageTranslation(ctx context.Context, sel ast.SelectionSet, v *models.LanguageTranslation) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + +func (ec *executionContext) marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v *models.Media) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Media(ctx, sel, v) +} + +func (ec *executionContext) marshalOMediaEXIF2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaEXIF(ctx context.Context, sel ast.SelectionSet, v *models.MediaEXIF) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._MediaEXIF(ctx, sel, v) +} + +func (ec *executionContext) marshalOMediaURL2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx context.Context, sel ast.SelectionSet, v *models.MediaURL) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._MediaURL(ctx, sel, v) +} + +func (ec *executionContext) unmarshalOOrderDirection2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrderDirection(ctx context.Context, v any) (*models.OrderDirection, error) { + if v == nil { + return nil, nil + } + var res = new(models.OrderDirection) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOOrderDirection2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrderDirection(ctx context.Context, sel ast.SelectionSet, v *models.OrderDirection) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + +func (ec *executionContext) unmarshalOOrdering2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐOrdering(ctx context.Context, v any) (*models.Ordering, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputOrdering(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalOPagination2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPagination(ctx context.Context, v any) (*models.Pagination, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputPagination(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalOShareTokenCredentials2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx context.Context, v any) (*models.ShareTokenCredentials, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputShareTokenCredentials(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v any) (*string, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalString(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalString(*v) + return res +} + +func (ec *executionContext) unmarshalOTime2ᚖtimeᚐTime(ctx context.Context, v any) (*time.Time, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalTime(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel ast.SelectionSet, v *time.Time) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalTime(*v) + return res +} + +func (ec *executionContext) marshalOVideoMetadata2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx context.Context, sel ast.SelectionSet, v *models.VideoMetadata) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._VideoMetadata(ctx, sel, v) +} + +func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx context.Context, sel ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Schema(ctx, sel, v) +} + +func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +// endregion ***************************** type.gotpl ***************************** diff --git a/api/graphql/models/actions/actions_test.go b/api/graphql/models/actions/actions_test.go new file mode 100644 index 0000000..7103384 --- /dev/null +++ b/api/graphql/models/actions/actions_test.go @@ -0,0 +1,11 @@ +package actions_test + +import ( + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} diff --git a/api/graphql/models/actions/album_actions.go b/api/graphql/models/actions/album_actions.go new file mode 100644 index 0000000..20f5c68 --- /dev/null +++ b/api/graphql/models/actions/album_actions.go @@ -0,0 +1,188 @@ +package actions + +import ( + "github.com/photoview/photoview/api/graphql/models" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +func MyAlbums(db *gorm.DB, user *models.User, order *models.Ordering, paginate *models.Pagination, + onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) { + + if err := user.FillAlbums(db); err != nil { + return nil, err + } + + if len(user.Albums) == 0 { + return nil, nil + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + query := db.Model(models.Album{}).Where("id IN (?)", userAlbumIDs) + + if onlyRoot != nil && *onlyRoot { + + singleRootAlbumID := getSingleRootAlbumID(user) + + if singleRootAlbumID != -1 && len(user.Albums) > 1 { + query = query.Where("parent_album_id = ?", singleRootAlbumID) + } else { + query = query.Where("parent_album_id IS NULL OR parent_album_id NOT IN (?)", userAlbumIDs) + } + } + + query = favoritesQuery(showEmpty, db, onlyWithFavorites, user, query) + + query = models.FormatSQL(query, order, paginate) + + var albums []*models.Album + if err := query.Find(&albums).Error; err != nil { + return nil, err + } + + return albums, nil +} + +func getSingleRootAlbumID(user *models.User) int { + var singleRootAlbumID int = -1 + for _, album := range user.Albums { + if album.ParentAlbumID == nil { + if singleRootAlbumID == -1 { + singleRootAlbumID = album.ID + } else { + singleRootAlbumID = -1 + break + } + } + } + return singleRootAlbumID +} + +func favoritesQuery(showEmpty *bool, db *gorm.DB, onlyWithFavorites *bool, user *models.User, query *gorm.DB) *gorm.DB { + if showEmpty == nil || !*showEmpty { + subQuery := db.Model(&models.Media{}).Where("album_id = albums.id") + + if onlyWithFavorites != nil && *onlyWithFavorites { + favoritesSubquery := db. + Model(&models.UserMediaData{UserID: user.ID}). + Where("user_media_data.media_id = media.id"). + Where("user_media_data.favorite = true") + + subQuery = subQuery.Where("EXISTS (?)", favoritesSubquery) + } + + query = query.Where("EXISTS (?)", subQuery) + } + return query +} + +func Album(db *gorm.DB, user *models.User, id int) (*models.Album, error) { + var album models.Album + if err := db.First(&album, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("album not found") + } + return nil, err + } + + ownsAlbum, err := user.OwnsAlbum(db, &album) + if err != nil { + return nil, err + } + + if !ownsAlbum { + return nil, errors.New("forbidden") + } + + return &album, nil +} + +func AlbumPath(db *gorm.DB, user *models.User, album *models.Album) ([]*models.Album, error) { + var albumPath []*models.Album + + err := db.Raw(` + WITH recursive path_albums AS ( + SELECT * FROM albums anchor WHERE anchor.id = ? + UNION + SELECT parent.* FROM path_albums child JOIN albums parent ON parent.id = child.parent_album_id + ) + SELECT * FROM path_albums WHERE id != ? + `, album.ID, album.ID).Scan(&albumPath).Error + + // Make sure to only return albums this user owns + for i := len(albumPath) - 1; i >= 0; i-- { + album := albumPath[i] + + owns, err := user.OwnsAlbum(db, album) + if err != nil { + return nil, err + } + + if !owns { + albumPath = albumPath[i+1:] + break + } + + } + + if err != nil { + return nil, err + } + + return albumPath, nil +} + +func SetAlbumCover(db *gorm.DB, user *models.User, mediaID int) (*models.Album, error) { + var media models.Media + + if err := db.Find(&media, mediaID).Error; err != nil { + return nil, err + } + + var album models.Album + + if err := db.Find(&album, &media.AlbumID).Error; err != nil { + return nil, err + } + + ownsAlbum, err := user.OwnsAlbum(db, &album) + if err != nil { + return nil, err + } + + if !ownsAlbum { + return nil, errors.New("forbidden") + } + + if err := db.Model(&album).Update("cover_id", mediaID).Error; err != nil { + return nil, err + } + + return &album, nil +} + +func ResetAlbumCover(db *gorm.DB, user *models.User, albumID int) (*models.Album, error) { + var album models.Album + if err := db.Find(&album, albumID).Error; err != nil { + return nil, err + } + + ownsAlbum, err := user.OwnsAlbum(db, &album) + if err != nil { + return nil, err + } + + if !ownsAlbum { + return nil, errors.New("forbidden") + } + + if err := db.Model(&album).Update("cover_id", nil).Error; err != nil { + return nil, err + } + + return &album, nil +} diff --git a/api/graphql/models/actions/album_actions_test.go b/api/graphql/models/actions/album_actions_test.go new file mode 100644 index 0000000..97704c0 --- /dev/null +++ b/api/graphql/models/actions/album_actions_test.go @@ -0,0 +1,382 @@ +package actions_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestAlbumPath(t *testing.T) { + db := test_utils.DatabaseTest(t) + + album := models.Album{ + Title: "Three", + Path: "/one/two/three", + ParentAlbum: &models.Album{ + Title: "Two", + Path: "/one/two", + ParentAlbum: &models.Album{ + Title: "One", + Path: "/one", + }, + }, + } + + assert.NoError(t, db.Save(&album).Error) + + user, err := models.RegisterUser(db, "user", nil, false) + assert.NoError(t, err) + + db.Model(&user).Association("Albums").Append(album.ParentAlbum.ParentAlbum) + + albumPath, err := actions.AlbumPath(db, user, &album) + assert.NoError(t, err) + assert.Len(t, albumPath, 2) + assert.Equal(t, "Two", albumPath[0].Title) + assert.Equal(t, "One", albumPath[1].Title) +} + +func TestAlbumCover(t *testing.T) { + db := test_utils.DatabaseTest(t) + + rootAlbum := models.Album{ + Title: "root", + Path: "/photos", + } + + if !assert.NoError(t, db.Save(&rootAlbum).Error) { + return + } + + children := []models.Album{ + { + Title: "child1", + Path: "/photos/child1", + ParentAlbumID: &rootAlbum.ID, + }, + { + Title: "child2", + Path: "/photos/child2", + ParentAlbumID: &rootAlbum.ID, + }, + } + + if !assert.NoError(t, db.Save(&children).Error) { + return + } + + photos := []models.Media{ + { + Title: "pic1", + Path: "/photos/pic1", + AlbumID: rootAlbum.ID, + }, + { + Title: "pic2", + Path: "/photos/pic2", + AlbumID: rootAlbum.ID, + }, + { + Title: "pic3", + Path: "/photos/child1/pic3", + AlbumID: children[0].ID, + }, + { + Title: "pic4", + Path: "/photos/child1/pic4", + AlbumID: children[0].ID, + }, + { + Title: "pic5", + Path: "/photos/child2/pic5", + AlbumID: children[1].ID, + }, + { + Title: "pic6", + Path: "/photos/child2/pic6", + AlbumID: children[1].ID, + }, + } + + if !assert.NoError(t, db.Save(&photos).Error) { + return + } + + if !assert.NoError(t, db.Model(&children[0]).Update("cover_id", &photos[3].ID).Error) { + return + } + + photoUrls := []models.MediaURL{ + { + MediaID: photos[0].ID, + Media: &photos[0], + }, + { + MediaID: photos[1].ID, + Media: &photos[1], + }, + { + MediaID: photos[2].ID, + Media: &photos[2], + }, + { + MediaID: photos[3].ID, + Media: &photos[3], + }, + { + MediaID: photos[4].ID, + Media: &photos[4], + }, + { + MediaID: photos[5].ID, + Media: &photos[5], + }, + } + + if !assert.NoError(t, db.Save(&photoUrls).Error) { + return + } + + userPass := "password" + regularUser, err := models.RegisterUser(db, "user1", &userPass, false) + if !assert.NoError(t, err) { + return + } + + if !assert.NoError(t, db.Model(®ularUser).Association("Albums").Append(&rootAlbum)) { + return + } + + if !assert.NoError(t, db.Model(®ularUser).Association("Albums").Append(&children)) { + return + } + + // Single test since we cannot rely on the tests being performed sequentially + t.Run("Album get and reset cover photos", func(t *testing.T) { + { + album, err := actions.Album(db, regularUser, rootAlbum.ID) + assert.NoError(t, err) + + albumThumb, err := album.Thumbnail(db) + assert.NoError(t, err) + + // Should return the latest photo since no coverID has been set + assert.EqualValues(t, "pic6", albumThumb.Title) + } + + { + album, err := actions.Album(db, regularUser, children[0].ID) + assert.NoError(t, err) + + albumThumb, err := album.Thumbnail(db) + assert.NoError(t, err) + + // coverID has already been set + assert.EqualValues(t, "pic4", albumThumb.Title) + } + + resetAlbum, err := actions.ResetAlbumCover(db, regularUser, children[0].ID) + assert.NoError(t, err) + + assert.Nil(t, resetAlbum.CoverID) + + resetThumb, err := resetAlbum.Thumbnail(db) + assert.NoError(t, err) + + assert.Equal(t, "pic4", resetThumb.Title) + }) + + t.Run("Album change cover photos", func(t *testing.T) { + assert.Nil(t, children[1].CoverID) + + album, err := actions.SetAlbumCover(db, regularUser, photos[4].ID) + assert.NoError(t, err) + + assert.Equal(t, children[1].ID, album.ID) + assert.NotNil(t, album.CoverID) + assert.Equal(t, photos[4].ID, *album.CoverID) + + albumThumb, err := album.Thumbnail(db) + assert.NoError(t, err) + + assert.Equal(t, photos[4].ID, albumThumb.ID) + }) + +} + +func TestAlbumsSingleRootExpand(t *testing.T) { + db := test_utils.DatabaseTest(t) + boolFalse := false + boolTrue := true + + unrelatedAlbum := models.Album{ + Title: "unrelated_album", + Path: "/another_place", + } + err := db.Create(&unrelatedAlbum).Error + assert.NoError(t, err) + + user, err := models.RegisterUser(db, "user", nil, false) + assert.NoError(t, err) + + rootAlbum := models.Album{ + Title: "root", + Path: "/root", + } + + err = db.Model(&user).Association("Albums").Replace(&rootAlbum) + assert.NoError(t, err) + + t.Run("Single root album, no children", func(t *testing.T) { + returnedAlbums, err := actions.MyAlbums(db, user, nil, nil, &boolTrue, &boolTrue, &boolFalse) + assert.NoError(t, err) + + assert.Len(t, returnedAlbums, 1) + }) + + childAlbums := []models.Album{ + { + Title: "child1", + Path: "/root/child1", + ParentAlbumID: &rootAlbum.ID, + }, + { + Title: "child2", + Path: "/root/child2", + ParentAlbumID: &rootAlbum.ID, + }, + { + Title: "child3", + Path: "/root/child3", + ParentAlbumID: &rootAlbum.ID, + }, + } + + err = db.Model(&user).Association("Albums").Append(&childAlbums) + assert.NoError(t, err) + + t.Run("Single root album, multiple children", func(t *testing.T) { + + returnedAlbums, err := actions.MyAlbums(db, user, nil, nil, &boolTrue, &boolTrue, &boolFalse) + assert.NoError(t, err) + + assert.Len(t, returnedAlbums, 3) + }) + +} + +// Related to #658 +func TestNonRootAlbumPath(t *testing.T) { + db := test_utils.DatabaseTest(t) + + boolTrue := true + boolFalse := false + + rootAlbum := models.Album{ + Title: "root", + Path: "/root", + } + + childAlbum := models.Album{ + Title: "child", + Path: "/root/child", + ParentAlbumID: &rootAlbum.ID, + } + + assert.NoError(t, db.Create(&rootAlbum).Error) + + // Register user + user, err := models.RegisterUser(db, "user", nil, false) + assert.NoError(t, err) + + // Assign album to user + err = db.Model(&user).Association("Albums").Append(&childAlbum) + assert.NoError(t, err) + + // The child album is a "local root album" for the user, as it does not have access to the root album + t.Run("User should only see child album", func(t *testing.T) { + returnedAlbums, err := actions.MyAlbums(db, user, nil, nil, &boolTrue, &boolTrue, &boolFalse) + assert.NoError(t, err) + + assert.Len(t, returnedAlbums, 1) + assert.Equal(t, "child", returnedAlbums[0].Title) + }) +} + +// Related to #658 +func TestNonRootAlbumPathMultipleUsers(t *testing.T) { + db := test_utils.DatabaseTest(t) + + boolTrue := true + boolFalse := false + + rootAlbum := models.Album{ + Title: "root", + Path: "/root", + } + + child1Album := models.Album{ + Title: "child1", + Path: "/root/child1", + ParentAlbumID: &rootAlbum.ID, + } + + child2Album := models.Album{ + Title: "child2", + Path: "/root/child2", + ParentAlbumID: &rootAlbum.ID, + } + + // Admin should have access to all albums + adminAlbums := []*models.Album{&rootAlbum, &child1Album, &child2Album} + + assert.NoError(t, db.Create(&rootAlbum).Error) + + // Register users + admin, err := models.RegisterUser(db, "admin", nil, false) + assert.NoError(t, err) + + user1, err := models.RegisterUser(db, "user1", nil, false) + assert.NoError(t, err) + + user2, err := models.RegisterUser(db, "user2", nil, false) + assert.NoError(t, err) + + // Assign albums to users + err = db.Model(&admin).Association("Albums").Append(&adminAlbums) + assert.NoError(t, err) + + err = db.Model(&user1).Association("Albums").Append(&child1Album) + assert.NoError(t, err) + + err = db.Model(&user2).Association("Albums").Append(&child2Album) + assert.NoError(t, err) + + t.Run("Admin should see all albums", func(t *testing.T) { + returnedAlbums, err := actions.MyAlbums(db, admin, nil, nil, &boolTrue, &boolTrue, &boolFalse) + assert.NoError(t, err) + + assert.Len(t, returnedAlbums, 2) + assert.Equal(t, "child1", returnedAlbums[0].Title) + assert.Equal(t, "child2", returnedAlbums[1].Title) + }) + + t.Run("User 1 should only see child1 album", func(t *testing.T) { + returnedAlbums, err := actions.MyAlbums(db, user1, nil, nil, &boolTrue, &boolTrue, &boolFalse) + assert.NoError(t, err) + + assert.Len(t, returnedAlbums, 1) + assert.Equal(t, "child1", returnedAlbums[0].Title) + }) + + t.Run("User 2 should only see child2 album", func(t *testing.T) { + returnedAlbums, err := actions.MyAlbums(db, user2, nil, nil, &boolTrue, &boolTrue, &boolFalse) + assert.NoError(t, err) + + assert.Len(t, returnedAlbums, 1) + assert.Equal(t, "child2", returnedAlbums[0].Title) + }) +} diff --git a/api/graphql/models/actions/media_actions.go b/api/graphql/models/actions/media_actions.go new file mode 100644 index 0000000..5793eba --- /dev/null +++ b/api/graphql/models/actions/media_actions.go @@ -0,0 +1,23 @@ +package actions + +import ( + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +func MyMedia(db *gorm.DB, user *models.User, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) { + if err := user.FillAlbums(db); err != nil { + return nil, err + } + + query := db.Where("media.album_id IN (SELECT user_albums.album_id FROM user_albums WHERE user_albums.user_id = ?)", + user.ID) + query = models.FormatSQL(query, order, paginate) + + var media []*models.Media + if err := query.Find(&media).Error; err != nil { + return nil, err + } + + return media, nil +} diff --git a/api/graphql/models/actions/media_actions_test.go b/api/graphql/models/actions/media_actions_test.go new file mode 100644 index 0000000..7a1eab6 --- /dev/null +++ b/api/graphql/models/actions/media_actions_test.go @@ -0,0 +1,88 @@ +package actions_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestMyMedia(t *testing.T) { + db := test_utils.DatabaseTest(t) + + password := "1234" + user, err := models.RegisterUser(db, "user", &password, false) + assert.NoError(t, err) + + rootAlbum := models.Album{ + Title: "root", + Path: "/photos", + } + + assert.NoError(t, db.Save(&rootAlbum).Error) + + childAlbum := models.Album{ + Title: "subalbum", + Path: "/photos/subalbum", + ParentAlbumID: &rootAlbum.ID, + } + + assert.NoError(t, db.Save(&childAlbum).Error) + + assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum)) + assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum)) + + media := []models.Media{ + { + Title: "pic1", + Path: "/photos/pic1", + AlbumID: rootAlbum.ID, + }, + { + Title: "pic2", + Path: "/photos/pic2", + AlbumID: rootAlbum.ID, + }, + { + Title: "pic3", + Path: "/photos/subalbum/pic3", + AlbumID: childAlbum.ID, + }, + { + Title: "pic4", + Path: "/photos/subalbum/pic4", + AlbumID: childAlbum.ID, + }, + } + + assert.NoError(t, db.Save(&media).Error) + + anotherUser, err := models.RegisterUser(db, "user2", &password, false) + assert.NoError(t, err) + + anotherAlbum := models.Album{ + Title: "AnotherAlbum", + Path: "/another", + } + + assert.NoError(t, db.Save(&anotherAlbum).Error) + + anotherMedia := models.Media{ + Title: "anotherPic", + Path: "/another/anotherPic", + AlbumID: anotherAlbum.ID, + } + + assert.NoError(t, db.Save(&anotherMedia).Error) + + assert.NoError(t, db.Model(&anotherUser).Association("Albums").Append(&anotherAlbum)) + + t.Run("Simple query", func(t *testing.T) { + myMedia, err := actions.MyMedia(db, user, nil, nil) + + assert.NoError(t, err) + assert.Len(t, myMedia, 4) + }) +} diff --git a/api/graphql/models/actions/search_actions.go b/api/graphql/models/actions/search_actions.go new file mode 100644 index 0000000..0e58ddf --- /dev/null +++ b/api/graphql/models/actions/search_actions.go @@ -0,0 +1,76 @@ +package actions + +import ( + "strings" + + "github.com/photoview/photoview/api/database/drivers" + "github.com/photoview/photoview/api/graphql/models" + "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func Search(db *gorm.DB, query string, userID int, limitMedia *int, limitAlbums *int) (*models.SearchResult, error) { + limitMediaInternal := 10 + limitAlbumsInternal := 10 + + if limitMedia != nil { + limitMediaInternal = *limitMedia + } + + if limitAlbums != nil { + limitAlbumsInternal = *limitAlbums + } + + wildQuery := "%" + strings.ToLower(query) + "%" + + var media []*models.Media + + userSubquery := db.Table("user_albums").Where("user_id = ?", userID) + if drivers.POSTGRES.MatchDatabase(db) { + userSubquery = userSubquery.Where("album_id = \"Album\".id") + } else { + userSubquery = userSubquery.Where("album_id = Album.id") + } + + err := db.Joins("Album"). + Where("EXISTS (?)", userSubquery). + Where("LOWER(media.title) LIKE ? OR LOWER(media.path) LIKE ?", wildQuery, wildQuery). + Clauses(clause.OrderBy{ + Expression: clause.Expr{ + SQL: "(CASE WHEN LOWER(media.title) LIKE ? THEN 2 WHEN LOWER(media.path) LIKE ? THEN 1 END) DESC", + Vars: []interface{}{wildQuery, wildQuery}, + WithoutParentheses: true}, + }). + Limit(limitMediaInternal).Find(&media).Error + + if err != nil { + return nil, errors.Wrapf(err, "searching media") + } + + var albums []*models.Album + + err = db. + Where("EXISTS (?)", db.Table("user_albums").Where("user_id = ?", userID).Where("album_id = albums.id")). + Where("albums.title LIKE ? OR albums.path LIKE ?", wildQuery, wildQuery). + Clauses(clause.OrderBy{ + Expression: clause.Expr{ + SQL: "(CASE WHEN albums.title LIKE ? THEN 2 WHEN albums.path LIKE ? THEN 1 END) DESC", + Vars: []interface{}{wildQuery, wildQuery}, + WithoutParentheses: true}, + }). + Limit(limitAlbumsInternal). + Find(&albums).Error + + if err != nil { + return nil, errors.Wrapf(err, "searching albums") + } + + result := models.SearchResult{ + Query: query, + Media: media, + Albums: albums, + } + + return &result, nil +} diff --git a/api/graphql/models/actions/search_actions_test.go b/api/graphql/models/actions/search_actions_test.go new file mode 100644 index 0000000..73aaf99 --- /dev/null +++ b/api/graphql/models/actions/search_actions_test.go @@ -0,0 +1,102 @@ +package actions_test + +import ( + "fmt" + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestSearch(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user, err := models.RegisterUser(db, "user", nil, true) + assert.NoError(t, err) + + rootAlbum := models.Album{ + Title: "root_album", + Path: "/media/", + } + + assert.NoError(t, db.Create(&rootAlbum).Error) + assert.NoError(t, db.Model(&rootAlbum).Association("Owners").Append(user)) + + type Result struct { + ID int + UserID int + AlbumID int + } + + mediaTitles := []string{ + "SOME_IMAGE.jpg", + "imageA.jpg", + "imageB.jpg", + "imageC.jpg", + "movie.mp4", + "person.png", + "123.png", + "ABC.gif", + "dog.mov", + "cat.mov", + "IMG_3255.JPG", + "IMG_5532.JPG", + "IMG_5533.JPG", + "IMG_5534.JPG", + "IMG_5535.JPG", + "IMG_5536.JPG", + } + + for _, mediaTitle := range mediaTitles { + image := models.Media{ + Title: mediaTitle, + Path: fmt.Sprintf("/media/%s", mediaTitle), + AlbumID: rootAlbum.ID, + } + assert.NoError(t, db.Create(&image).Error) + } + + type SearchTest = struct { + query string + userID int + limitMedia *int + limitAlbum *int + + expectedMediaCount int + expectedAlbumCount int + } + + searchTests := []SearchTest{ + { + query: "image", + userID: user.ID, + expectedMediaCount: 4, + expectedAlbumCount: 0, + }, + { + query: "g", + userID: user.ID, + expectedMediaCount: 10, + expectedAlbumCount: 0, + }, + { + query: "media", + userID: user.ID, + expectedMediaCount: 10, + expectedAlbumCount: 1, + }, + } + + for _, test := range searchTests { + t.Run(fmt.Sprintf("Search query: '%s'", test.query), func(t *testing.T) { + result, err := actions.Search(db, test.query, test.userID, test.limitMedia, test.limitAlbum) + assert.NoError(t, err) + + assert.Equal(t, result.Query, test.query) + assert.Len(t, result.Albums, test.expectedAlbumCount) + assert.Len(t, result.Media, test.expectedMediaCount) + }) + } +} diff --git a/api/graphql/models/actions/share_token_actions.go b/api/graphql/models/actions/share_token_actions.go new file mode 100644 index 0000000..c4fcd89 --- /dev/null +++ b/api/graphql/models/actions/share_token_actions.go @@ -0,0 +1,184 @@ +package actions + +import ( + "time" + + "github.com/photoview/photoview/api/database/drivers" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func AddMediaShare(db *gorm.DB, user *models.User, mediaID int, expire *time.Time, password *string) (*models.ShareToken, + error) { + + var media models.Media + + var query string + if drivers.POSTGRES.MatchDatabase(db) { + query = "EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = \"Album\".id AND user_albums.user_id = ?)" + } else { + query = "EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = Album.id AND user_albums.user_id = ?)" + } + + err := db.Joins("Album"). + Where(query, user.ID). + First(&media, mediaID). + Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, auth.ErrUnauthorized + } else { + return nil, errors.Wrap(err, "failed to validate media owner with database") + } + } + + hashedPassword, err := hashSharePassword(password) + if err != nil { + return nil, err + } + + shareToken := models.ShareToken{ + Value: utils.GenerateToken(), + OwnerID: user.ID, + Expire: expire, + Password: hashedPassword, + AlbumID: nil, + MediaID: &mediaID, + } + + if err := db.Create(&shareToken).Error; err != nil { + return nil, errors.Wrap(err, "failed to insert new share token into database") + } + + return &shareToken, nil +} + +func AddAlbumShare(db *gorm.DB, user *models.User, albumID int, expire *time.Time, password *string) (*models.ShareToken, + error) { + + var count int64 + err := db. + Model(&models.Album{}). + Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = albums.id AND user_albums.user_id = ?)", + user.ID). + Count(&count).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to validate album owner with database") + } + + if count == 0 { + return nil, auth.ErrUnauthorized + } + + var hashedPassword *string = nil + if password != nil { + hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12) + if err != nil { + return nil, errors.Wrap(err, "failed to hash token password") + } + hashedStr := string(hashedPassBytes) + hashedPassword = &hashedStr + } + + shareToken := models.ShareToken{ + Value: utils.GenerateToken(), + OwnerID: user.ID, + Expire: expire, + Password: hashedPassword, + AlbumID: &albumID, + MediaID: nil, + } + + if err := db.Create(&shareToken).Error; err != nil { + return nil, errors.Wrap(err, "failed to insert new share token into database") + } + + return &shareToken, nil +} + +func DeleteShareToken(db *gorm.DB, userID int, tokenValue string) (*models.ShareToken, error) { + token, err := getUserToken(db, userID, tokenValue) + if err != nil { + return nil, err + } + + if err := db.Delete(&token).Error; err != nil { + return nil, errors.Wrapf(err, "failed to delete share token (%s) from database", tokenValue) + } + + return token, nil +} + +func ProtectShareToken(db *gorm.DB, userID int, tokenValue string, password *string) (*models.ShareToken, error) { + token, err := getUserToken(db, userID, tokenValue) + if err != nil { + return nil, err + } + + hashedPassword, err := hashSharePassword(password) + if err != nil { + return nil, err + } + + token.Password = hashedPassword + + if err := db.Save(&token).Error; err != nil { + return nil, errors.Wrap(err, "failed to update password for share token") + } + + return token, nil +} + +func SetExpireShareToken(db *gorm.DB, userID int, tokenValue string, expire *time.Time) (*models.ShareToken, error) { + token, err := getUserToken(db, userID, tokenValue) + if err != nil { + return nil, err + } + + token.Expire = expire + + if err := db.Save(&token).Error; err != nil { + return nil, errors.Wrap(err, "failed to update the expiration date for share token") + } + + return token, nil +} + +func hashSharePassword(password *string) (*string, error) { + var hashedPassword *string = nil + if password != nil { + hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12) + if err != nil { + return nil, errors.Wrap(err, "failed to generate hash for share password") + } + hashedStr := string(hashedPassBytes) + hashedPassword = &hashedStr + } + + return hashedPassword, nil +} + +func getUserToken(db *gorm.DB, userID int, tokenValue string) (*models.ShareToken, error) { + + var query string + if drivers.POSTGRES.MatchDatabase(db) { + query = "\"Owner\".id = ? OR \"Owner\".admin = TRUE" + } else { + query = "Owner.id = ? OR Owner.admin = TRUE" + } + + var token models.ShareToken + err := db.Where("share_tokens.value = ?", tokenValue).Joins("Owner").Where(query, userID).First(&token).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to get user share token from database") + } + + return &token, nil +} diff --git a/api/graphql/models/actions/share_token_actions_test.go b/api/graphql/models/actions/share_token_actions_test.go new file mode 100644 index 0000000..e1029c0 --- /dev/null +++ b/api/graphql/models/actions/share_token_actions_test.go @@ -0,0 +1,120 @@ +package actions_test + +import ( + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestShareToken(t *testing.T) { + db := test_utils.DatabaseTest(t) + + password := "1234" + user, err := models.RegisterUser(db, "user", &password, false) + assert.NoError(t, err) + + rootAlbum := models.Album{ + Title: "root", + Path: "/photos", + } + + assert.NoError(t, db.Save(&rootAlbum).Error) + + childAlbum := models.Album{ + Title: "subalbum", + Path: "/photos/subalbum", + ParentAlbumID: &rootAlbum.ID, + } + + assert.NoError(t, db.Save(&childAlbum).Error) + + assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum)) + assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum)) + + media := []models.Media{ + { + Title: "pic1", + Path: "/photos/pic1", + AlbumID: rootAlbum.ID, + }, + { + Title: "pic3", + Path: "/photos/subalbum/pic3", + AlbumID: childAlbum.ID, + }, + { + Title: "pic4", + Path: "/photos/subalbum/pic4", + AlbumID: childAlbum.ID, + }, + } + + assert.NoError(t, db.Save(&media).Error) + + expireTime := time.Unix(1632866400, 0) + sharePassword := "secretSharePassword" + + var mediaShare *models.ShareToken + var albumShare *models.ShareToken + + t.Run("Add album share", func(t *testing.T) { + share, err := actions.AddAlbumShare(db, user, rootAlbum.ID, &expireTime, nil) + albumShare = share + + assert.NoError(t, err) + assert.NotNil(t, share) + + assert.NotEmpty(t, share.Value) + assert.Equal(t, rootAlbum.ID, *share.AlbumID) + assert.Nil(t, share.MediaID) + }) + + t.Run("Add media share", func(t *testing.T) { + share, err := actions.AddMediaShare(db, user, media[0].ID, &expireTime, &sharePassword) + mediaShare = share + + assert.NoError(t, err) + assert.NotNil(t, share) + + assert.NotEmpty(t, share.Value) + assert.Equal(t, media[0].ID, *share.MediaID) + assert.Nil(t, share.AlbumID) + }) + + t.Run("Delete share token", func(t *testing.T) { + deletedShare, err := actions.DeleteShareToken(db, user.ID, mediaShare.Value) + + assert.NoError(t, err) + assert.Equal(t, mediaShare.ID, deletedShare.ID) + }) + + t.Run("Protect share token", func(t *testing.T) { + + assert.Empty(t, albumShare.Password) + + share, err := actions.ProtectShareToken(db, user.ID, albumShare.Value, &sharePassword) + assert.NoError(t, err) + assert.NotEmpty(t, share.Password) + + share, err = actions.ProtectShareToken(db, user.ID, albumShare.Value, nil) + assert.NoError(t, err) + assert.Empty(t, share.Password) + }) + + t.Run("Set Expiration date for share token", func(t *testing.T) { + assert.NotEmpty(t, albumShare.Expire) + time_ := time.Date(2025, 12, 6, 0, 0, 0, 0, time.UTC) + + share, err := actions.SetExpireShareToken(db, user.ID, albumShare.Value, &time_) + assert.NoError(t, err) + assert.Equal(t, time_, *share.Expire) + + share, err = actions.SetExpireShareToken(db, user.ID, albumShare.Value, nil) + assert.NoError(t, err) + assert.Nil(t, share.Expire) + }) +} diff --git a/api/graphql/models/actions/timeline_actions.go b/api/graphql/models/actions/timeline_actions.go new file mode 100644 index 0000000..5af7da5 --- /dev/null +++ b/api/graphql/models/actions/timeline_actions.go @@ -0,0 +1,62 @@ +package actions + +import ( + "time" + + "github.com/photoview/photoview/api/database/drivers" + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +func MyTimeline(db *gorm.DB, user *models.User, paginate *models.Pagination, onlyFavorites *bool, + fromDate *time.Time) ([]*models.Media, error) { + + const albumsTitleASC = "albums.title ASC" + + query := db. + Joins("JOIN albums ON media.album_id = albums.id"). + Where("albums.id IN (?)", db.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID)) + + switch drivers.GetDatabaseDriverType(db) { + case drivers.POSTGRES: + query = query. + Order("DATE_TRUNC('year', date_shot) DESC"). + Order("DATE_TRUNC('month', date_shot) DESC"). + Order("DATE_TRUNC('day', date_shot) DESC"). + Order(albumsTitleASC). + Order("media.date_shot DESC") + case drivers.SQLITE: + query = query. + Order("strftime('%Y-%m-%d', media.date_shot) DESC"). // convert to YYYY-MM-DD + Order(albumsTitleASC). + Order("TIME(media.date_shot) DESC") + default: + query = query. + Order("YEAR(media.date_shot) DESC"). + Order("MONTH(media.date_shot) DESC"). + Order("DAY(media.date_shot) DESC"). + Order(albumsTitleASC). + Order("TIME(media.date_shot) DESC") + } + + if fromDate != nil { + query = query.Where("media.date_shot < ?", fromDate) + } + + if onlyFavorites != nil && *onlyFavorites { + query = query. + Where("media.id IN (?)", db.Table("user_media_data"). + Select("user_media_data.media_id"). + Where("user_media_data.user_id = ?", user.ID). + Where("user_media_data.favorite")) + } + + query = models.FormatSQL(query, nil, paginate) + + var media []*models.Media + if err := query.Find(&media).Error; err != nil { + return nil, err + } + + return media, nil +} diff --git a/api/graphql/models/actions/timeline_actions_test.go b/api/graphql/models/actions/timeline_actions_test.go new file mode 100644 index 0000000..9e738b5 --- /dev/null +++ b/api/graphql/models/actions/timeline_actions_test.go @@ -0,0 +1,117 @@ +package actions_test + +import ( + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestMyTimeline(t *testing.T) { + db := test_utils.DatabaseTest(t) + + password := "1234" + user, err := models.RegisterUser(db, "user", &password, false) + assert.NoError(t, err) + + rootAlbum := models.Album{ + Title: "root", + Path: "/photos", + } + + assert.NoError(t, db.Save(&rootAlbum).Error) + + childAlbum := models.Album{ + Title: "subalbum", + Path: "/photos/subalbum", + ParentAlbumID: &rootAlbum.ID, + } + + assert.NoError(t, db.Save(&childAlbum).Error) + + assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum)) + assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum)) + + media := []models.Media{ + { + Title: "pic1", + Path: "/photos/pic1", + AlbumID: rootAlbum.ID, + DateShot: time.Unix(1632758400, 0), // Sep 27 2021 16:00:00 + }, + { + Title: "pic2", + Path: "/photos/pic2", + AlbumID: rootAlbum.ID, + DateShot: time.Unix(1628762400, 0), // Aug 12 2021 10:00:00 + }, + { + Title: "pic3", + Path: "/photos/subalbum/pic3", + AlbumID: childAlbum.ID, + DateShot: time.Unix(1632763800, 0), // Sep 27 2021 17:30:00 + }, + { + Title: "pic4", + Path: "/photos/subalbum/pic4", + AlbumID: childAlbum.ID, + DateShot: time.Unix(1628775900, 0), // Aug 12 2021 13:45:00 + }, + } + + assert.NoError(t, db.Save(&media).Error) + + _, err = user.FavoriteMedia(db, media[0].ID, true) + assert.NoError(t, err) + + // Add media not owned by first user + anotherUser, err := models.RegisterUser(db, "user2", &password, false) + assert.NoError(t, err) + + anotherAlbum := models.Album{ + Title: "AnotherAlbum", + Path: "/another", + } + + assert.NoError(t, db.Save(&anotherAlbum).Error) + + anotherMedia := models.Media{ + Title: "anotherPic", + Path: "/another/anotherPic", + AlbumID: anotherAlbum.ID, + } + + assert.NoError(t, db.Save(&anotherMedia).Error) + assert.NoError(t, db.Model(&anotherUser).Association("Albums").Append(&anotherAlbum)) + + t.Run("MyTimeline with no filters", func(t *testing.T) { + timelineMedia, err := actions.MyTimeline(db, user, nil, nil, nil) + + assert.NoError(t, err) + assert.Len(t, timelineMedia, 4) + + for i, title := range []string{"pic1", "pic3", "pic2", "pic4"} { + assert.Equalf(t, timelineMedia[i].Title, title, "Element %d didn't match: got %s expected %s", i, timelineMedia[i].Title, title) + } + + }) + + t.Run("MyTimeline with only favorites", func(t *testing.T) { + favorites := true + timelineMedia, err := actions.MyTimeline(db, user, nil, &favorites, nil) + + assert.NoError(t, err) + assert.Len(t, timelineMedia, 1) + }) + + t.Run("MyTimeline before date", func(t *testing.T) { + beforeDate := time.Unix(1629792000, 0) // Aug 24 2021 08:00:00 + timelineMedia, err := actions.MyTimeline(db, user, nil, nil, &beforeDate) + + assert.NoError(t, err) + assert.Len(t, timelineMedia, 2) + }) +} diff --git a/api/graphql/models/actions/user_actions.go b/api/graphql/models/actions/user_actions.go new file mode 100644 index 0000000..31eb71c --- /dev/null +++ b/api/graphql/models/actions/user_actions.go @@ -0,0 +1,84 @@ +package actions + +import ( + "errors" + "os" + "path" + "strconv" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/utils" + "gorm.io/gorm" +) + +func DeleteUser(db *gorm.DB, userID int) (*models.User, error) { + + // make sure the last admin user is not deleted + var adminUsers []*models.User + db.Model(&models.User{}).Where("admin = true").Limit(2).Find(&adminUsers) + if len(adminUsers) == 1 && adminUsers[0].ID == userID { + return nil, errors.New("deleting sole admin user is not allowed") + } + + var user models.User + deletedAlbumIDs := make([]int, 0) + + var err error + err = db.Transaction(func(tx *gorm.DB) error { + if err = tx.First(&user, userID).Error; err != nil { + return err + } + + userAlbums := user.Albums + if err = tx.Model(&user).Association("Albums").Find(&userAlbums); err != nil { + return err + } + + if err = tx.Model(&user).Association("Albums").Clear(); err != nil { + return err + } + + deletedAlbumIDs, err = deleteNotOwnedAlbums(userAlbums, tx, deletedAlbumIDs) + if err != nil { + return err + } + + if err = tx.Delete(&user).Error; err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + // If there is only one associated user, clean up the cache folder and delete the album row + return &user, cleanup(deletedAlbumIDs) +} + +func cleanup(deletedAlbumIDs []int) error { + var err error + for _, deletedAlbumID := range deletedAlbumIDs { + cachePath := path.Join(utils.MediaCachePath(), strconv.Itoa(int(deletedAlbumID))) + if err = os.RemoveAll(cachePath); err != nil { + return err + } + } + return err +} + +func deleteNotOwnedAlbums(userAlbums []models.Album, tx *gorm.DB, deletedAlbumIDs []int) ([]int, error) { + for _, album := range userAlbums { + var associatedUsers = tx.Model(album).Association("Owners").Count() + + if associatedUsers == 0 { + deletedAlbumIDs = append(deletedAlbumIDs, album.ID) + if err := tx.Delete(album).Error; err != nil { + return nil, err + } + } + } + return deletedAlbumIDs, nil +} diff --git a/api/graphql/models/actions/user_actions_test.go b/api/graphql/models/actions/user_actions_test.go new file mode 100644 index 0000000..6ab6952 --- /dev/null +++ b/api/graphql/models/actions/user_actions_test.go @@ -0,0 +1,82 @@ +package actions_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestDeleteUser(t *testing.T) { + t.Run("Delete regular user", func(t *testing.T) { + db := test_utils.DatabaseTest(t) + + adminUser, err := models.RegisterUser(db, "admin", nil, true) + assert.NoError(t, err) + + regularUser, err := models.RegisterUser(db, "regular", nil, false) + assert.NoError(t, err) + + var dbUsers []*models.User + err = db.Model(models.User{}).Find(&dbUsers).Error + assert.NoError(t, err) + assert.Len(t, dbUsers, 2) + + deletedUser, err := actions.DeleteUser(db, regularUser.ID) + assert.NoError(t, err) + assert.Equal(t, regularUser.ID, deletedUser.ID) + + err = db.Model(models.User{}).Find(&dbUsers).Error + assert.NoError(t, err) + assert.Len(t, dbUsers, 1) + assert.Equal(t, adminUser.ID, dbUsers[0].ID) + }) + + t.Run("Try to delete sole admin user", func(t *testing.T) { + db := test_utils.DatabaseTest(t) + + adminUser, err := models.RegisterUser(db, "admin", nil, true) + assert.NoError(t, err) + + _, err = models.RegisterUser(db, "regular", nil, false) + assert.NoError(t, err) + + var dbUsers []*models.User + err = db.Model(models.User{}).Find(&dbUsers).Error + assert.NoError(t, err) + assert.Len(t, dbUsers, 2) + + _, err = actions.DeleteUser(db, adminUser.ID) + assert.Error(t, err) + + err = db.Model(models.User{}).Find(&dbUsers).Error + assert.NoError(t, err) + assert.Len(t, dbUsers, 2) + }) + + t.Run("Delete admin user when multiple admins exist", func(t *testing.T) { + db := test_utils.DatabaseTest(t) + + adminUser1, err := models.RegisterUser(db, "admin", nil, true) + assert.NoError(t, err) + + adminUser2, err := models.RegisterUser(db, "another_admin", nil, true) + assert.NoError(t, err) + + var dbUsers []*models.User + err = db.Model(models.User{}).Find(&dbUsers).Error + assert.NoError(t, err) + assert.Len(t, dbUsers, 2) + + deletedUser, err := actions.DeleteUser(db, adminUser1.ID) + assert.NoError(t, err) + assert.Equal(t, adminUser1.ID, deletedUser.ID) + + err = db.Model(models.User{}).Find(&dbUsers).Error + assert.NoError(t, err) + assert.Len(t, dbUsers, 1) + assert.Equal(t, adminUser2.ID, dbUsers[0].ID) + }) +} diff --git a/api/graphql/models/album.go b/api/graphql/models/album.go new file mode 100644 index 0000000..82fbabb --- /dev/null +++ b/api/graphql/models/album.go @@ -0,0 +1,115 @@ +package models + +import ( + "crypto/md5" + "encoding/hex" + + "gorm.io/gorm" +) + +type Album struct { + Model + Title string `gorm:"not null"` + ParentAlbumID *int `gorm:"index"` + ParentAlbum *Album `gorm:"constraint:OnDelete:SET NULL;"` + // OwnerID int `gorm:"not null"` + // Owner User + Owners []User `gorm:"many2many:user_albums;constraint:OnDelete:CASCADE;"` + Path string `gorm:"not null"` + PathHash string `gorm:"unique"` + CoverID *int +} + +func (a *Album) FilePath() string { + return a.Path +} + +func (a *Album) BeforeSave(tx *gorm.DB) (err error) { + hash := md5.Sum([]byte(a.Path)) + a.PathHash = hex.EncodeToString(hash[:]) + return nil +} + +// GetChildren performs a recursive query to get all the children of the album. +// An optional filter can be provided that can be used to modify the query on the children. +func (a *Album) GetChildren(db *gorm.DB, filter func(*gorm.DB) *gorm.DB) (children []*Album, err error) { + return GetChildrenFromAlbums(db, filter, []int{a.ID}) +} + +func GetChildrenFromAlbums(db *gorm.DB, filter func(*gorm.DB) *gorm.DB, albumIDs []int) (children []*Album, err error) { + query := db.Model(&Album{}).Table("sub_albums") + + if filter != nil { + query = filter(query) + } + + err = db.Raw(` + WITH recursive sub_albums AS ( + SELECT * FROM albums AS root WHERE id IN (?) + UNION ALL + SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id + ) + + ? + `, albumIDs, query).Find(&children).Error + + return children, err +} + +func (a *Album) GetParents(db *gorm.DB, filter func(*gorm.DB) *gorm.DB) (parents []*Album, err error) { + return GetParentsFromAlbums(db, filter, a.ID) +} + +func GetParentsFromAlbums(db *gorm.DB, filter func(*gorm.DB) *gorm.DB, albumID int) (parents []*Album, err error) { + query := db.Model(&Album{}).Table("super_albums") + + if filter != nil { + query = filter(query) + } + + err = db.Raw(` + WITH recursive super_albums AS ( + SELECT * FROM albums AS leaf WHERE id = ? + UNION ALL + SELECT parent.* from albums AS parent JOIN super_albums ON parent.id = super_albums.parent_album_id + ) + + ? + `, albumID, query).Find(&parents).Error + + return parents, err +} + +func (a *Album) Thumbnail(db *gorm.DB) (*Media, error) { + var media Media + + if a.CoverID != nil { + if err := db.First(&media, *a.CoverID).Error; err != nil { + return nil, err + } + return &media, nil + } + + query := ` + WITH RECURSIVE sub_albums AS ( + SELECT id FROM albums WHERE id = ? + UNION ALL + SELECT children.id FROM albums AS children + INNER JOIN sub_albums ON children.parent_album_id = sub_albums.id + ) + SELECT * FROM media + WHERE media.album_id IN (SELECT id FROM sub_albums) + ORDER BY media.id DESC + LIMIT 1 + ` + + if err := db.Raw(query, a.ID).Scan(&media).Error; err != nil { + return nil, err + } + + if media.ID == 0 { + return nil, nil // Return nil for empty albums + } + + return &media, nil +} diff --git a/api/graphql/models/album_test.go b/api/graphql/models/album_test.go new file mode 100644 index 0000000..31cc752 --- /dev/null +++ b/api/graphql/models/album_test.go @@ -0,0 +1,369 @@ +package models_test + +import ( + "fmt" + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestAlbumGetChildrenAndParents(t *testing.T) { + const photosPath = "/photos" + const photosChild1Path = "/photos/child1" + const photosChild1SubchildPath = "/photos/child1/subchild" + db := test_utils.DatabaseTest(t) + + rootAlbum := models.Album{ + Title: "root", + Path: photosPath, + } + + if !assert.NoError(t, db.Save(&rootAlbum).Error) { + return + } + + children := []models.Album{ + { + Title: "child1", + Path: photosChild1Path, + ParentAlbumID: &rootAlbum.ID, + }, + { + Title: "child2", + Path: "/photos/child2", + ParentAlbumID: &rootAlbum.ID, + }, + { + Title: "not_child", + Path: "/videos", + }, + } + + if !assert.NoError(t, db.Save(&children).Error) { + return + } + + subChild := models.Album{ + Title: "subchild", + Path: photosChild1SubchildPath, + ParentAlbumID: &children[0].ID, + } + + if !assert.NoError(t, db.Save(&subChild).Error) { + return + } + + verifyResult := func(t *testing.T, expectedAlbums []*models.Album, result []*models.Album) { + assert.Equal(t, len(expectedAlbums), len(result)) + + for _, expected := range expectedAlbums { + foundExpected := false + for _, item := range result { + if item.Title == expected.Title && item.Path == expected.Path { + foundExpected = true + break + } + } + if !foundExpected { + assert.Failf(t, "albums did not match", "expected to find item: %v", expected) + } + } + } + + t.Run("Album get children", func(t *testing.T) { + rootChildren, err := rootAlbum.GetChildren(db, nil) + if !assert.NoError(t, err) { + return + } + + expectedChildren := []*models.Album{ + { + Title: "root", + Path: photosPath, + }, + { + Title: "child1", + Path: photosChild1Path, + }, + { + Title: "child2", + Path: "/photos/child2", + }, + { + Title: "subchild", + Path: photosChild1SubchildPath, + }, + } + + verifyResult(t, expectedChildren, rootChildren) + }) + + t.Run("Album get parents", func(t *testing.T) { + parents, err := subChild.GetParents(db, nil) + if !assert.NoError(t, err) { + return + } + + expectedParents := []*models.Album{ + { + Title: "root", + Path: photosPath, + }, + { + Title: "child1", + Path: photosChild1Path, + }, + { + Title: "subchild", + Path: photosChild1SubchildPath, + }, + } + + verifyResult(t, expectedParents, parents) + }) + +} + +func TestAlbumThumbnail(t *testing.T) { + db := test_utils.DatabaseTest(t) + + mediaAlbum := models.Album{ + Title: "Media album", + Path: "/media_album", + } + if !assert.NoError(t, db.Save(&mediaAlbum).Error) { + return + } + + media := models.Media{ + Path: "thumb.jpg", + AlbumID: mediaAlbum.ID, + } + if !assert.NoError(t, db.Save(&media).Error) { + return + } + + t.Run("Thumbnail from CoverID", func(t *testing.T) { + album := models.Album{ + Title: "Album with cover", + Path: "/cover_album", + CoverID: &media.ID, + } + if !assert.NoError(t, db.Save(&album).Error) { + return + } + + result, err := album.Thumbnail(db) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, media.ID, result.ID) + }) + + t.Run("Thumbnail from child media", func(t *testing.T) { + parentAlbum := models.Album{ + Title: "Parent album", + Path: "/parent", + } + if !assert.NoError(t, db.Save(&parentAlbum).Error) { + return + } + + childAlbum := models.Album{ + Title: "Child album", + Path: "/parent/child", + ParentAlbumID: &parentAlbum.ID, + } + if !assert.NoError(t, db.Save(&childAlbum).Error) { + return + } + + childMedia := models.Media{ + Path: "child_media.jpg", + AlbumID: childAlbum.ID, + } + if !assert.NoError(t, db.Save(&childMedia).Error) { + return + } + + result, err := parentAlbum.Thumbnail(db) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, childMedia.ID, result.ID) + }) + + t.Run("Empty album with no media", func(t *testing.T) { + emptyAlbum := models.Album{ + Title: "Empty album", + Path: "/empty", + } + if !assert.NoError(t, db.Save(&emptyAlbum).Error) { + return + } + + result, err := emptyAlbum.Thumbnail(db) + assert.NoError(t, err) + assert.Nil(t, result, "Empty albums should have nil thumbnail") + }) + + t.Run("Thumbnail from grandchild media", func(t *testing.T) { + // Create grandparent-parent-child relationship with media only in child + grandparentAlbum := models.Album{ + Title: "Grandparent", + Path: "/grandparent", + } + if !assert.NoError(t, db.Save(&grandparentAlbum).Error) { + return + } + + parentAlbum := models.Album{ + Title: "Parent", + Path: "/grandparent/parent", + ParentAlbumID: &grandparentAlbum.ID, + } + if !assert.NoError(t, db.Save(&parentAlbum).Error) { + return + } + + childAlbum := models.Album{ + Title: "Child", + Path: "/grandparent/parent/child", + ParentAlbumID: &parentAlbum.ID, + } + if !assert.NoError(t, db.Save(&childAlbum).Error) { + return + } + + childMedia := models.Media{ + Path: "deep_media.jpg", + AlbumID: childAlbum.ID, + } + if !assert.NoError(t, db.Save(&childMedia).Error) { + return + } + + result, err := grandparentAlbum.Thumbnail(db) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, childMedia.ID, result.ID) + }) + + t.Run("CoverID takes precedence over any media", func(t *testing.T) { + // Create album with both direct media and a cover ID + priorityAlbum := models.Album{ + Title: "Priority album", + Path: "/priority", + CoverID: &media.ID, // Using existing media as cover + } + if !assert.NoError(t, db.Save(&priorityAlbum).Error) { + return + } + + // Add direct media to the album with unique path + directMedia := models.Media{ + Path: fmt.Sprintf("direct_media_%d.jpg", time.Now().UnixNano()), + AlbumID: priorityAlbum.ID, + } + if !assert.NoError(t, db.Save(&directMedia).Error) { + return + } + + // Test that CoverID takes precedence + result, err := priorityAlbum.Thumbnail(db) + assert.NoError(t, err) + assert.Equal(t, media.ID, result.ID, "CoverID should take precedence over direct media") + }) + + t.Run("Some media is returned when multiple exist in hierarchy", func(t *testing.T) { + // Create a parent album + parentAlbum := models.Album{ + Title: "Parent album", + Path: "/parent_media_test", + } + if !assert.NoError(t, db.Save(&parentAlbum).Error) { + return + } + + // Add direct media to parent with unique path + parentMedia := models.Media{ + Path: fmt.Sprintf("parent_media_%d.jpg", time.Now().UnixNano()), + AlbumID: parentAlbum.ID, + } + if !assert.NoError(t, db.Save(&parentMedia).Error) { + return + } + + // Create child album with media + childAlbum := models.Album{ + Title: "Child album", + Path: "/parent_media_test/child", + ParentAlbumID: &parentAlbum.ID, + } + if !assert.NoError(t, db.Save(&childAlbum).Error) { + return + } + + // Add child media with unique path + childMedia := models.Media{ + Path: fmt.Sprintf("child_media_%d.jpg", time.Now().UnixNano()), + AlbumID: childAlbum.ID, + } + if !assert.NoError(t, db.Save(&childMedia).Error) { + return + } + + // Test that some media is returned + result, err := parentAlbum.Thumbnail(db) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.ID == parentMedia.ID || result.ID == childMedia.ID, + "Should return either direct media or child album media") + t.Logf("For reference - Selected: %d, Parent media: %d, Child media: %d", + result.ID, parentMedia.ID, childMedia.ID) + }) + + t.Run("Database order determines which media is selected", func(t *testing.T) { + // Create album with multiple media + multiMediaAlbum := models.Album{ + Title: "Album with multiple media", + Path: "/multi_media", + } + if !assert.NoError(t, db.Save(&multiMediaAlbum).Error) { + return + } + + // Add multiple media to the album with unique paths + mediaItems := []models.Media{ + {Path: fmt.Sprintf("media1_%d.jpg", time.Now().UnixNano()), AlbumID: multiMediaAlbum.ID}, + // Sleep briefly to ensure different timestamps + {Path: fmt.Sprintf("media2_%d.jpg", time.Now().UnixNano()+1), AlbumID: multiMediaAlbum.ID}, + {Path: fmt.Sprintf("media3_%d.jpg", time.Now().UnixNano()+2), AlbumID: multiMediaAlbum.ID}, + } + if !assert.NoError(t, db.Save(&mediaItems).Error) { + return + } + + // Test which media is selected + result, err := multiMediaAlbum.Thumbnail(db) + assert.NoError(t, err) + assert.NotNil(t, result) + + // Log which item was selected for documentation purposes + t.Logf("Selected media ID: %d", result.ID) + for i, item := range mediaItems { + t.Logf("Media %d: ID %d, Path %s", i+1, item.ID, item.Path) + } + + // Verify one of our media items was selected + found := false + for _, item := range mediaItems { + if result.ID == item.ID { + found = true + break + } + } + assert.True(t, found, "One of the album's media should be selected") + }) +} diff --git a/api/graphql/models/base.go b/api/graphql/models/base.go new file mode 100644 index 0000000..2c56b38 --- /dev/null +++ b/api/graphql/models/base.go @@ -0,0 +1,15 @@ +package models + +import ( + "time" +) + +type Model struct { + ID int `gorm:"primarykey"` + ModelTimestamps +} + +type ModelTimestamps struct { + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/api/graphql/models/face_detection.go b/api/graphql/models/face_detection.go new file mode 100644 index 0000000..7fff5eb --- /dev/null +++ b/api/graphql/models/face_detection.go @@ -0,0 +1,130 @@ +package models + +import ( + "bytes" + "database/sql/driver" + "encoding/binary" + "fmt" + "strconv" + "strings" + + "github.com/photoview/photoview/api/database/drivers" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +type FaceGroup struct { + Model + Label *string + ImageFaces []ImageFace `gorm:"constraint:OnDelete:CASCADE;"` +} + +type ImageFace struct { + Model + FaceGroupID int `gorm:"not null;index"` + FaceGroup *FaceGroup + MediaID int `gorm:"not null;index"` + Media Media `gorm:"constraint:OnDelete:CASCADE;"` + Descriptor FaceDescriptor `gorm:"not null"` + Rectangle FaceRectangle `gorm:"not null"` +} + +func (f *ImageFace) FillMedia(db *gorm.DB) error { + if f.Media.ID != 0 { + // media already exists + return nil + } + + if err := db.Model(&f).Association("Media").Find(&f.Media); err != nil { + return err + } + + return nil +} + +type FaceDescriptor [128]float32 // same as go-face's Descriptor + +// GormDataType datatype used in database +func (FaceDescriptor) GormDBDataType(db *gorm.DB, field *schema.Field) string { + switch drivers.GetDatabaseDriverType(db) { + case drivers.MYSQL, drivers.SQLITE: + return "BLOB" + case drivers.POSTGRES: + return "BYTEA" + } + return "" +} + +// Scan tells GORM how to convert database data to Go format +func (fd *FaceDescriptor) Scan(value interface{}) error { + byteValue := value.([]byte) + reader := bytes.NewReader(byteValue) + binary.Read(reader, binary.LittleEndian, fd) + return nil +} + +// Value tells GORM how to save into the database +func (fd FaceDescriptor) Value() (driver.Value, error) { + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.LittleEndian, fd); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// FaceRectangle stores a relative rectangle of a face in an image. +type FaceRectangle struct { + MinX, MaxX float64 + MinY, MaxY float64 +} + +// GormDataType datatype used in database +func (fr FaceRectangle) GormDataType() string { + return "VARCHAR(64)" +} + +// Scan tells GORM how to convert database data to Go format +func (fr *FaceRectangle) Scan(value interface{}) error { + stringArray, ok := value.(string) + if !ok { + byteArray := value.([]uint8) + stringArray = string(byteArray) + } + + slices := strings.Split(stringArray, ":") + + if len(slices) != 4 { + return fmt.Errorf("invalid face rectangle format, expected 4 values, got %d", len(slices)) + } + + var err error + + fr.MinX, err = strconv.ParseFloat(slices[0], 32) + if err != nil { + return err + } + + fr.MaxX, err = strconv.ParseFloat(slices[1], 32) + if err != nil { + return err + } + + fr.MinY, err = strconv.ParseFloat(slices[2], 32) + if err != nil { + return err + } + + fr.MaxY, err = strconv.ParseFloat(slices[3], 32) + if err != nil { + return err + } + + return nil +} + +// Value tells GORM how to save into the database +func (fr FaceRectangle) Value() (driver.Value, error) { + result := fmt.Sprintf("%f:%f:%f:%f", fr.MinX, fr.MaxX, fr.MinY, fr.MaxY) + return result, nil +} diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go new file mode 100644 index 0000000..c5f1097 --- /dev/null +++ b/api/graphql/models/generated.go @@ -0,0 +1,316 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package models + +import ( + "bytes" + "fmt" + "io" + "strconv" + "time" +) + +type AuthorizeResult struct { + Success bool `json:"success"` + // A textual status message describing the result, can be used to show an error message when `success` is false + Status string `json:"status"` + // An access token used to authenticate new API requests as the newly authorized user. Is present when success is true + Token *string `json:"token,omitempty"` +} + +type Coordinates struct { + // GPS latitude in degrees + Latitude float64 `json:"latitude"` + // GPS longitude in degrees + Longitude float64 `json:"longitude"` +} + +type MediaDownload struct { + // A description of the role of the media file + Title string `json:"title"` + MediaURL *MediaURL `json:"mediaUrl"` +} + +type Mutation struct { +} + +type Notification struct { + // A key used to identify the notification, new notification updates with the same key, should replace the old notifications + Key string `json:"key"` + Type NotificationType `json:"type"` + // The text for the title of the notification + Header string `json:"header"` + // The text for the body of the notification + Content string `json:"content"` + // A value between 0 and 1 when the notification type is `Progress` + Progress *float64 `json:"progress,omitempty"` + // Whether or not the message of the notification is positive, the UI might reflect this with a green color + Positive bool `json:"positive"` + // Whether or not the message of the notification is negative, the UI might reflect this with a red color + Negative bool `json:"negative"` + // Time in milliseconds before the notification should close + Timeout *int `json:"timeout,omitempty"` +} + +// Used to specify how to sort items +type Ordering struct { + // A column in the database to order by + OrderBy *string `json:"order_by,omitempty"` + OrderDirection *OrderDirection `json:"order_direction,omitempty"` +} + +// Used to specify pagination on a list of items +type Pagination struct { + // How many items to maximally fetch + Limit *int `json:"limit,omitempty"` + // How many items to skip from the beginning of the query, specified by the `Ordering` + Offset *int `json:"offset,omitempty"` +} + +type Query struct { +} + +type ScannerResult struct { + Finished bool `json:"finished"` + Success bool `json:"success"` + Progress *float64 `json:"progress,omitempty"` + Message *string `json:"message,omitempty"` +} + +type SearchResult struct { + // The string that was searched for + Query string `json:"query"` + // A list of albums that matched the query + Albums []*Album `json:"albums"` + // A list of media that matched the query + Media []*Media `json:"media"` +} + +// Credentials used to identify and authenticate a share token +type ShareTokenCredentials struct { + Token string `json:"token"` + Password *string `json:"password,omitempty"` +} + +type Subscription struct { +} + +// A group of media from the same album and the same day, that is grouped together in a timeline view +// NOTE: It isn't used. Just copy from the old schema.graphql. +type TimelineGroup struct { + // The full album containing the media in this timeline group + Album *Album `json:"album"` + // The media contained in this timeline group + Media []*Media `json:"media"` + // The total amount of media in this timeline group + MediaTotal int `json:"mediaTotal"` + // The day shared for all media in this timeline group + Date time.Time `json:"date"` +} + +// Supported language translations of the user interface +type LanguageTranslation string + +const ( + LanguageTranslationEnglish LanguageTranslation = "English" + LanguageTranslationFrench LanguageTranslation = "French" + LanguageTranslationItalian LanguageTranslation = "Italian" + LanguageTranslationSwedish LanguageTranslation = "Swedish" + LanguageTranslationDanish LanguageTranslation = "Danish" + LanguageTranslationSpanish LanguageTranslation = "Spanish" + LanguageTranslationPolish LanguageTranslation = "Polish" + LanguageTranslationUkrainian LanguageTranslation = "Ukrainian" + LanguageTranslationGerman LanguageTranslation = "German" + LanguageTranslationRussian LanguageTranslation = "Russian" + LanguageTranslationTraditionalChineseTw LanguageTranslation = "TraditionalChineseTW" + LanguageTranslationTraditionalChineseHk LanguageTranslation = "TraditionalChineseHK" + LanguageTranslationSimplifiedChinese LanguageTranslation = "SimplifiedChinese" + LanguageTranslationPortuguese LanguageTranslation = "Portuguese" + LanguageTranslationBasque LanguageTranslation = "Basque" + LanguageTranslationTurkish LanguageTranslation = "Turkish" + LanguageTranslationJapanese LanguageTranslation = "Japanese" + LanguageTranslationDutch LanguageTranslation = "Dutch" +) + +var AllLanguageTranslation = []LanguageTranslation{ + LanguageTranslationEnglish, + LanguageTranslationFrench, + LanguageTranslationItalian, + LanguageTranslationSwedish, + LanguageTranslationDanish, + LanguageTranslationSpanish, + LanguageTranslationPolish, + LanguageTranslationUkrainian, + LanguageTranslationGerman, + LanguageTranslationRussian, + LanguageTranslationTraditionalChineseTw, + LanguageTranslationTraditionalChineseHk, + LanguageTranslationSimplifiedChinese, + LanguageTranslationPortuguese, + LanguageTranslationBasque, + LanguageTranslationTurkish, + LanguageTranslationJapanese, + LanguageTranslationDutch, +} + +func (e LanguageTranslation) IsValid() bool { + switch e { + case LanguageTranslationEnglish, LanguageTranslationFrench, LanguageTranslationItalian, LanguageTranslationSwedish, LanguageTranslationDanish, LanguageTranslationSpanish, LanguageTranslationPolish, LanguageTranslationUkrainian, LanguageTranslationGerman, LanguageTranslationRussian, LanguageTranslationTraditionalChineseTw, LanguageTranslationTraditionalChineseHk, LanguageTranslationSimplifiedChinese, LanguageTranslationPortuguese, LanguageTranslationBasque, LanguageTranslationTurkish, LanguageTranslationJapanese, LanguageTranslationDutch: + return true + } + return false +} + +func (e LanguageTranslation) String() string { + return string(e) +} + +func (e *LanguageTranslation) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = LanguageTranslation(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid LanguageTranslation", str) + } + return nil +} + +func (e LanguageTranslation) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *LanguageTranslation) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e LanguageTranslation) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +// Specified the type a particular notification is of +type NotificationType string + +const ( + // A regular message with no special additions + NotificationTypeMessage NotificationType = "Message" + // A notification with an attached progress indicator + NotificationTypeProgress NotificationType = "Progress" + // Close a notification with a given key + NotificationTypeClose NotificationType = "Close" +) + +var AllNotificationType = []NotificationType{ + NotificationTypeMessage, + NotificationTypeProgress, + NotificationTypeClose, +} + +func (e NotificationType) IsValid() bool { + switch e { + case NotificationTypeMessage, NotificationTypeProgress, NotificationTypeClose: + return true + } + return false +} + +func (e NotificationType) String() string { + return string(e) +} + +func (e *NotificationType) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = NotificationType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid NotificationType", str) + } + return nil +} + +func (e NotificationType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *NotificationType) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e NotificationType) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + +// Used to specify which order to sort items in +type OrderDirection string + +const ( + // Sort accending A-Z + OrderDirectionAsc OrderDirection = "ASC" + // Sort decending Z-A + OrderDirectionDesc OrderDirection = "DESC" +) + +var AllOrderDirection = []OrderDirection{ + OrderDirectionAsc, + OrderDirectionDesc, +} + +func (e OrderDirection) IsValid() bool { + switch e { + case OrderDirectionAsc, OrderDirectionDesc: + return true + } + return false +} + +func (e OrderDirection) String() string { + return string(e) +} + +func (e *OrderDirection) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = OrderDirection(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid OrderDirection", str) + } + return nil +} + +func (e OrderDirection) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *OrderDirection) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e OrderDirection) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go new file mode 100644 index 0000000..0da79ab --- /dev/null +++ b/api/graphql/models/media.go @@ -0,0 +1,156 @@ +package models + +import ( + "fmt" + "path" + "strconv" + "strings" + "time" + + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type Media struct { + Model + Title string `gorm:"not null"` + Path string `gorm:"not null"` + PathHash string `gorm:"not null;unique"` + AlbumID int `gorm:"not null;index"` + Album Album `gorm:"constraint:OnDelete:CASCADE;"` + ExifID *int `gorm:"index"` + Exif *MediaEXIF `gorm:"constraint:OnDelete:CASCADE;"` + MediaURL []MediaURL `gorm:"constraint:OnDelete:CASCADE;"` + DateShot time.Time `gorm:"not null"` + Type MediaType `gorm:"not null;index"` + VideoMetadataID *int `gorm:"index"` + VideoMetadata *VideoMetadata `gorm:"constraint:OnDelete:CASCADE;"` + SideCarPath *string + SideCarHash *string `gorm:"unique"` + Faces []*ImageFace `gorm:"constraint:OnDelete:CASCADE;"` + Blurhash *string `gorm:""` +} + +func (Media) TableName() string { + return "media" +} + +func (m *Media) BeforeSave(tx *gorm.DB) error { + // Update path hash + m.PathHash = MD5Hash(m.Path) + + return nil +} + +func (m *Media) Date() time.Time { + return m.DateShot +} + +func (m *Media) GetThumbnail() (*MediaURL, error) { + if len(m.MediaURL) == 0 { + return nil, errors.New("media.MediaURL is empty") + } + + for _, url := range m.MediaURL { + if url.Purpose == PhotoThumbnail || url.Purpose == VideoThumbnail { + url.Media = m + return &url, nil + } + } + + return nil, nil +} + +func (m *Media) GetHighRes() (*MediaURL, error) { + if len(m.MediaURL) == 0 { + return nil, errors.New("media.MediaURL is empty") + } + + for _, url := range m.MediaURL { + if url.Purpose == PhotoHighRes { + url.Media = m + return &url, nil + } + } + + return nil, nil +} + +func (m *Media) CachePath() (string, error) { + return utils.CachePathForMedia(m.AlbumID, m.ID) +} + +type MediaType string + +const ( + MediaTypePhoto MediaType = "photo" + MediaTypeVideo MediaType = "video" +) + +var AllMediaType = []MediaType{ + MediaTypePhoto, + MediaTypeVideo, +} + +type MediaPurpose string + +const ( + PhotoThumbnail MediaPurpose = "thumbnail" + PhotoHighRes MediaPurpose = "high-res" + MediaOriginal MediaPurpose = "original" + VideoWeb MediaPurpose = "video-web" + VideoThumbnail MediaPurpose = "video-thumbnail" +) + +type MediaURL struct { + Model + MediaID int `gorm:"not null;index"` + Media *Media `gorm:"constraint:OnDelete:CASCADE;"` + MediaName string `gorm:"not null;index"` + Width int `gorm:"not null"` + Height int `gorm:"not null"` + Purpose MediaPurpose `gorm:"not null;index"` + ContentType string `gorm:"not null"` + FileSize int64 `gorm:"not null"` +} + +func (p *MediaURL) URL() string { + + imageURL := utils.ApiEndpointUrl() + if p.Purpose != VideoWeb { + imageURL.Path = path.Join(imageURL.Path, "photo", p.MediaName) + } else { + imageURL.Path = path.Join(imageURL.Path, "video", p.MediaName) + } + + return imageURL.String() +} + +func (p *MediaURL) CachedPath() (string, error) { + var cachedPath string + + if p.Media == nil { + return "", errors.New("mediaURL.Media is nil") + } + + if p.Purpose == PhotoThumbnail || p.Purpose == PhotoHighRes || p.Purpose == VideoThumbnail || p.Purpose == VideoWeb { + cachedPath = path.Join(utils.MediaCachePath(), strconv.Itoa(int(p.Media.AlbumID)), strconv.Itoa(int(p.MediaID)), + p.MediaName) + } else if p.Purpose == MediaOriginal { + cachedPath = p.Media.Path + } else { + return "", errors.New(fmt.Sprintf("cannot determine cache path for purpose (%s)", p.Purpose)) + } + + return cachedPath, nil +} + +func SanitizeMediaName(mediaName string) string { + result := mediaName + result = strings.ReplaceAll(result, "/", "") + result = strings.ReplaceAll(result, "\\", "") + result = strings.ReplaceAll(result, " ", "_") + result = strings.ReplaceAll(result, ".", "_") + return result +} diff --git a/api/graphql/models/media_exif.go b/api/graphql/models/media_exif.go new file mode 100644 index 0000000..9d6bc1f --- /dev/null +++ b/api/graphql/models/media_exif.go @@ -0,0 +1,70 @@ +package models + +import ( + "fmt" + "time" +) + +type MediaEXIF struct { + Model + Description *string + Camera *string + Maker *string + Lens *string + DateShot *time.Time + OffsetSecShot *int + Exposure *float64 + Aperture *float64 + Iso *int64 + FocalLength *float64 + Flash *int64 + Orientation *int64 + ExposureProgram *int64 + GPSLatitude *float64 + GPSLongitude *float64 +} + +func (MediaEXIF) TableName() string { + return "media_exif" +} + +func (exif *MediaEXIF) Media() *Media { + panic("not implemented") +} + +func (exif *MediaEXIF) Coordinates() *Coordinates { + if exif.GPSLatitude == nil || exif.GPSLongitude == nil { + return nil + } + + return &Coordinates{ + Latitude: *exif.GPSLatitude, + Longitude: *exif.GPSLongitude, + } +} + +const rfc3339WithoutTimezone = "2006-01-02T15:04:05.999" + +func (exif *MediaEXIF) DateShotWithOffset() *string { + if exif.DateShot == nil { + return nil + } + dateShot := exif.DateShot.UTC() + dateNoTimezone := dateShot.Format(rfc3339WithoutTimezone) + + if exif.OffsetSecShot == nil { + return &dateNoTimezone + } + + offsetAbs := *exif.OffsetSecShot + sign := "+" + if offsetAbs < 0 { + offsetAbs = -offsetAbs + sign = "-" + } + hour := offsetAbs / 60 / 60 + minute := offsetAbs / 60 % 60 + date := fmt.Sprintf("%s%s%02d:%02d", dateNoTimezone, sign, hour, minute) + + return &date +} diff --git a/api/graphql/models/media_exif_test.go b/api/graphql/models/media_exif_test.go new file mode 100644 index 0000000..3aebadf --- /dev/null +++ b/api/graphql/models/media_exif_test.go @@ -0,0 +1,63 @@ +package models_test + +import ( + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" +) + +// Different database has different behavior when storing date with timezone. +// - SQLite: keep the original timezone +// - MySQL/MariaDB: store in UTC +// - PostgreSQL: store in the timezone of the db client +// We cannot maintain consistent behavior across different databases without the extra offset field. + +const ( + layout = "2006:01:02 15:04:05" + layoutWithOffset = "2006:01:02 15:04:05-07:00" +) + +func TestDatabaseReproduceDateWithOffset(t *testing.T) { + db := test_utils.DatabaseTest(t) + + tests := []struct { + name string + date string + offsetSec *int + want string + }{ + {"NoSubSecNoOffset", "2025:11:01 14:02:03", nil, "2025-11-01T14:02:03"}, + {"SubSecNoOffset", "2025:11:01 14:02:03.123", nil, "2025-11-01T14:02:03.123"}, + {"NoSubSecWithOffset", "2025:11:01 14:02:03", new(60 * 60), "2025-11-01T14:02:03+01:00"}, + {"SubSecWithOffset", "2025:11:01 14:02:03.123", new(60 * 60), "2025-11-01T14:02:03.123+01:00"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + date, err := time.ParseInLocation(layout, tc.date, time.UTC) + if err != nil { + t.Fatalf("parse time %q error: %v", tc.date, err) + } + + exif := models.MediaEXIF{ + DateShot: &date, + OffsetSecShot: tc.offsetSec, + } + + if err := db.Save(&exif).Error; err != nil { + t.Fatalf("store exif error: %v", err) + } + + var got models.MediaEXIF + if err := db.Where("id = ?", exif.ID).First(&got).Error; err != nil { + t.Fatalf("get exif error: %v", err) + } + + if got, want := *got.DateShotWithOffset(), tc.want; got != want { + t.Errorf("got = %q, want: %q", got, want) + } + }) + } +} diff --git a/api/graphql/models/media_test.go b/api/graphql/models/media_test.go new file mode 100644 index 0000000..80405e3 --- /dev/null +++ b/api/graphql/models/media_test.go @@ -0,0 +1,127 @@ +package models_test + +import ( + "fmt" + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/utils" + "github.com/stretchr/testify/assert" +) + +const mimeJpeg = "image/jpeg" + +func TestSanitizeMediaName(t *testing.T) { + tests := [][2]string{ + {"filename.png", "filename_png"}, + {"../..\\escape", "____escape"}, + {"..", "__"}, + {"..\\/", "__"}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("sanitize: %s", test[0]), func(t *testing.T) { + assert.Equal(t, test[1], models.SanitizeMediaName(test[0])) + }) + } +} + +func TestMediaURLCachePath(t *testing.T) { + mediaUrl := models.MediaURL{} + mediaUrl.Media = nil + + _, err := mediaUrl.CachedPath() + assert.EqualError(t, err, "mediaURL.Media is nil") + + mediaUrl = models.MediaURL{ + Purpose: models.PhotoThumbnail, + MediaID: 1, + Media: &models.Media{ + Model: models.Model{ + ID: 1, + }, + Title: "media.jpg", + AlbumID: 2, + }, + MediaName: "media_thumb.jpg", + } + + path, err := mediaUrl.CachedPath() + + assert.NoError(t, err) + assert.Equal(t, "media_cache/2/1/media_thumb.jpg", path) + +} + +func TestMediaURLGetURL(t *testing.T) { + t.Setenv(string(utils.EnvAPIEndpoint), "") + + photo := models.MediaURL{ + MediaName: "photo.jpg", + ContentType: mimeJpeg, + Purpose: models.PhotoHighRes, + } + + assert.Equal(t, "/api/photo/photo.jpg", photo.URL()) + + video := models.MediaURL{ + MediaName: "video.mp4", + ContentType: "video/mp4", + Purpose: models.VideoWeb, + } + + assert.Equal(t, "/api/video/video.mp4", video.URL()) +} + +func TestMediaGetThumbnail(t *testing.T) { + photo := models.Media{ + Title: "test.png", + Path: "path/test.png", + Type: models.MediaTypePhoto, + MediaURL: []models.MediaURL{ + { + MediaName: "photo.jpg", + ContentType: mimeJpeg, + Purpose: models.PhotoHighRes, + }, + { + MediaName: "thumbnail.jpg", + ContentType: mimeJpeg, + Purpose: models.PhotoThumbnail, + }, + { + MediaName: "photo.png", + ContentType: "image/png", + Purpose: models.MediaOriginal, + }, + }, + } + + thumb, err := photo.GetThumbnail() + assert.NoError(t, err) + assert.Equal(t, thumb.MediaName, "thumbnail.jpg") + assert.NotNil(t, thumb.Media) + + video := models.Media{ + Title: "video-test.mp4", + Path: "path/test.mp4", + Type: models.MediaTypeVideo, + MediaURL: []models.MediaURL{ + { + MediaName: "video.mp4", + ContentType: "video/mp4", + Purpose: models.VideoWeb, + }, + { + MediaName: "video-thumbnail.jpg", + ContentType: "image/jpg", + Purpose: models.VideoThumbnail, + }, + }, + } + + thumb, err = video.GetThumbnail() + assert.NoError(t, err) + assert.Equal(t, thumb.MediaName, "video-thumbnail.jpg") + assert.NotNil(t, thumb.Media) +} diff --git a/api/graphql/models/models_test.go b/api/graphql/models/models_test.go new file mode 100644 index 0000000..bc20c81 --- /dev/null +++ b/api/graphql/models/models_test.go @@ -0,0 +1,11 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} diff --git a/api/graphql/models/share_token.go b/api/graphql/models/share_token.go new file mode 100644 index 0000000..91e4fdb --- /dev/null +++ b/api/graphql/models/share_token.go @@ -0,0 +1,22 @@ +package models + +import ( + "time" +) + +type ShareToken struct { + Model + Value string `gorm:"not null"` + OwnerID int `gorm:"not null;index"` + Owner User `gorm:"constraint:OnDelete:CASCADE;"` + Expire *time.Time `gorm:"index"` + Password *string + AlbumID *int `gorm:"index"` + Album *Album `gorm:"constraint:OnDelete:CASCADE;"` + MediaID *int `gorm:"index"` + Media *Media `gorm:"constraint:OnDelete:CASCADE;"` +} + +func (share *ShareToken) Token() string { + return share.Value +} diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go new file mode 100644 index 0000000..d358adb --- /dev/null +++ b/api/graphql/models/site_info.go @@ -0,0 +1,52 @@ +package models + +import ( + db_drivers "github.com/photoview/photoview/api/database/drivers" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type SiteInfo struct { + InitialSetup bool `gorm:"not null"` + PeriodicScanInterval int `gorm:"not null"` + ConcurrentWorkers int `gorm:"not null"` +} + +func (SiteInfo) TableName() string { + return "site_info" +} + +func DefaultSiteInfo(db *gorm.DB) SiteInfo { + defaultConcurrentWorkers := 3 + if db_drivers.SQLITE.MatchDatabase(db) { + defaultConcurrentWorkers = 1 + } + + return SiteInfo{ + InitialSetup: true, + PeriodicScanInterval: 0, + ConcurrentWorkers: defaultConcurrentWorkers, + } +} + +// GetSiteInfo gets the site info row from the database, and creates it if it does not exist +func GetSiteInfo(db *gorm.DB) (*SiteInfo, error) { + + var siteInfo []*SiteInfo + + if err := db.Limit(1).Find(&siteInfo).Error; err != nil { + return nil, errors.Wrap(err, "get site info from database") + } + + if len(siteInfo) == 0 { + newSiteInfo := DefaultSiteInfo(db) + + if err := db.Create(&newSiteInfo).Error; err != nil { + return nil, errors.Wrap(err, "initialize site_info") + } + + return &newSiteInfo, nil + } else { + return siteInfo[0], nil + } +} diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go new file mode 100644 index 0000000..0881939 --- /dev/null +++ b/api/graphql/models/site_info_test.go @@ -0,0 +1,41 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestSiteInfo(t *testing.T) { + db := test_utils.DatabaseTest(t) + + site_info, err := models.GetSiteInfo(db) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, models.DefaultSiteInfo(db), *site_info) + + site_info.InitialSetup = false + site_info.PeriodicScanInterval = 360 + site_info.ConcurrentWorkers = 10 + + if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) { + return + } + + site_info, err = models.GetSiteInfo(db) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, models.SiteInfo{ + InitialSetup: false, + PeriodicScanInterval: 360, + ConcurrentWorkers: 10, + }, *site_info) + +} diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go new file mode 100644 index 0000000..53dfdb5 --- /dev/null +++ b/api/graphql/models/user.go @@ -0,0 +1,200 @@ +package models + +import ( + "crypto/rand" + "fmt" + "time" + + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type User struct { + Model + Username string `gorm:"unique;size:128"` + Password *string `gorm:"size:256"` + // RootPath string `gorm:"size:512` + Albums []Album `gorm:"many2many:user_albums;constraint:OnDelete:CASCADE;"` + Admin bool `gorm:"default:false"` +} + +type UserMediaData struct { + ModelTimestamps + UserID int `gorm:"primaryKey;autoIncrement:false"` + MediaID int `gorm:"primaryKey;autoIncrement:false"` + Favorite bool `gorm:"not null;default:false"` +} + +type UserAlbums struct { + UserID int `gorm:"primaryKey;autoIncrement:false;constraint:OnDelete:CASCADE;"` + AlbumID int `gorm:"primaryKey;autoIncrement:false;constraint:OnDelete:CASCADE;"` +} + +type AccessToken struct { + Model + UserID int `gorm:"not null;index"` + User User `gorm:"constraint:OnDelete:CASCADE;"` + Value string `gorm:"not null;size:24;index"` + Expire time.Time `gorm:"not null;index"` +} + +type UserPreferences struct { + Model + UserID int `gorm:"not null;index"` + User User `gorm:"constraint:OnDelete:CASCADE;"` + Language *LanguageTranslation +} + +func (u *UserPreferences) BeforeSave(tx *gorm.DB) error { + + if u.Language != nil && *u.Language == "" { + u.Language = nil + } + + if u.Language != nil { + langStr := string(*u.Language) + foundMatch := false + for _, lang := range AllLanguageTranslation { + if string(lang) == langStr { + foundMatch = true + break + } + } + + if !foundMatch { + return errors.New("invalid language value") + } + } + + return nil +} + +var ErrorInvalidUserCredentials = errors.New("invalid credentials") + +func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error) { + var user User + + result := db.Where("username = ?", username).First(&user) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrorInvalidUserCredentials + } + return nil, errors.Wrap(result.Error, "failed to get user by username when authorizing") + } + + if user.Password == nil { + return nil, errors.New("user does not have a password") + } + + if err := bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + return nil, ErrorInvalidUserCredentials + } else { + return nil, errors.Wrap(err, "compare user password hash") + } + } + + return &user, nil +} + +func RegisterUser(db *gorm.DB, username string, password *string, admin bool) (*User, error) { + user := User{ + Username: username, + Admin: admin, + } + + if password != nil { + hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12) + if err != nil { + return nil, errors.Wrap(err, "failed to hash password") + } + hashedPass := string(hashedPassBytes) + + user.Password = &hashedPass + } + + result := db.Create(&user) + if result.Error != nil { + return nil, errors.Wrap(result.Error, "insert new user with password into database") + } + + return &user, nil +} + +func (user *User) GenerateAccessToken(db *gorm.DB) (*AccessToken, error) { + bytes := make([]byte, 24) + if _, err := rand.Read(bytes); err != nil { + return nil, errors.New(fmt.Sprintf("Could not generate token: %s\n", err.Error())) + } + const CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + for i, b := range bytes { + bytes[i] = CHARACTERS[b%byte(len(CHARACTERS))] + } + + tokenValue := string(bytes) + expire := time.Now().Add(14 * 24 * time.Hour) + + token := AccessToken{ + UserID: user.ID, + Value: tokenValue, + Expire: expire, + } + + result := db.Create(&token) + if result.Error != nil { + return nil, errors.Wrap(result.Error, "saving access token to database") + } + + return &token, nil +} + +// FillAlbums fill user.Albums with albums from database +func (user *User) FillAlbums(db *gorm.DB) error { + // Albums already present + if len(user.Albums) > 0 { + return nil + } + + if err := db.Model(&user).Association("Albums").Find(&user.Albums); err != nil { + return errors.Wrap(err, "fill user albums") + } + + return nil +} + +func (user *User) OwnsAlbum(db *gorm.DB, album *Album) (bool, error) { + filter := func(query *gorm.DB) *gorm.DB { + return query.Where( + "EXISTS (SELECT 1 FROM user_albums WHERE user_albums.user_id = ? AND user_albums.album_id = id LIMIT 1)", + user.ID) + } + + ownedParents, err := album.GetParents(db, filter) + if err != nil { + return false, err + } + + return len(ownedParents) > 0, nil +} + +// FavoriteMedia sets/clears a media as favorite for the user +func (user *User) FavoriteMedia(db *gorm.DB, mediaID int, favorite bool) (*Media, error) { + userMediaData := UserMediaData{ + UserID: user.ID, + MediaID: mediaID, + Favorite: favorite, + } + + if err := db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&userMediaData).Error; err != nil { + return nil, errors.Wrapf(err, "update user favorite media in database") + } + + var media Media + if err := db.First(&media, mediaID).Error; err != nil { + return nil, errors.Wrap(err, "get media from database after favorite update") + } + + return &media, nil +} diff --git a/api/graphql/models/user_test.go b/api/graphql/models/user_test.go new file mode 100644 index 0000000..5be9ead --- /dev/null +++ b/api/graphql/models/user_test.go @@ -0,0 +1,231 @@ +package models_test + +import ( + "testing" + "time" + + "github.com/photoview/photoview/api/dataloader" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestUserRegistrationAuthorization(t *testing.T) { + db := test_utils.DatabaseTest(t) + + t.Run("Register user", func(t *testing.T) { + password := "1234" + user, err := models.RegisterUser(db, "admin", &password, true) + if !assert.NoError(t, err) { + return + } + + assert.NotNil(t, user) + assert.EqualValues(t, "admin", user.Username) + assert.NotNil(t, user.Password) + assert.NotEqualValues(t, "1234", user.Password) // should be hashed + assert.True(t, user.Admin) + }) + + t.Run("Authorize user", func(t *testing.T) { + user, err := models.AuthorizeUser(db, "admin", "1234") + if !assert.NoError(t, err) { + return + } + + assert.NotNil(t, user) + assert.EqualValues(t, "admin", user.Username) + }) + + t.Run("Authorize invalid credentials", func(t *testing.T) { + user, err := models.AuthorizeUser(db, "invalid_username", "1234") + assert.ErrorIs(t, err, models.ErrorInvalidUserCredentials) + assert.Nil(t, user) + + user, err = models.AuthorizeUser(db, "admin", "invalid_password") + assert.ErrorIs(t, err, models.ErrorInvalidUserCredentials) + assert.Nil(t, user) + }) +} + +func TestAccessToken(t *testing.T) { + db := test_utils.DatabaseTest(t) + + pass := "" + user := models.User{ + Username: "user1", + Password: &pass, + Admin: false, + } + + if !assert.NoError(t, db.Save(&user).Error) { + return + } + + access_token, err := user.GenerateAccessToken(db) + if !assert.NoError(t, err) { + return + } + + assert.NotNil(t, access_token) + assert.Equal(t, user.ID, access_token.UserID) + assert.NotEmpty(t, access_token.Value) + assert.True(t, access_token.Expire.After(time.Now())) +} + +func TestUserFillAlbums(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user := models.User{ + Username: "user", + } + + if !assert.NoError(t, db.Save(&user).Error) { + return + } + + err := user.FillAlbums(db) + assert.NoError(t, err) + assert.Empty(t, user.Albums) + + albums := []models.Album{ + { + Title: "album1", + Path: "/photos/album1", + }, + { + Title: "album2", + Path: "/photos/album2", + }, + } + + if !assert.NoError(t, db.Model(&user).Association("Albums").Append(&albums)) { + return + } + + user.Albums = make([]models.Album, 0) + + err = user.FillAlbums(db) + assert.NoError(t, err) + assert.Len(t, user.Albums, 2) + +} + +func TestUserOwnsAlbum(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user := models.User{ + Username: "user", + } + + if !assert.NoError(t, db.Save(&user).Error) { + return + } + + albums := []models.Album{ + { + Title: "album1", + Path: "/photos/album1", + }, + { + Title: "album2", + Path: "/photos/album2", + }, + } + + if !assert.NoError(t, db.Model(&user).Association("Albums").Append(&albums)) { + return + } + + subAlbums := []models.Album{ + { + Title: "subalbum1", + Path: "/photos/album2/subalbum1", + ParentAlbumID: &albums[1].ID, + }, + { + Title: "another_sub", + Path: "/photos/album2/another_sub", + ParentAlbumID: &albums[1].ID, + }, + { + Title: "subalbum2", + Path: "/photos/album1/subalbum2", + ParentAlbumID: &albums[0].ID, + }, + } + + if !assert.NoError(t, db.Model(&user).Association("Albums").Append(&subAlbums)) { + return + } + + for _, album := range albums { + owns, err := user.OwnsAlbum(db, &album) + assert.NoError(t, err) + assert.True(t, owns) + } + + for _, album := range subAlbums { + owns, err := user.OwnsAlbum(db, &album) + assert.NoError(t, err) + assert.True(t, owns) + } + + separateAlbum := models.Album{ + Title: "separate_album", + Path: "/my_media/album123", + } + + if !assert.NoError(t, db.Save(&separateAlbum).Error) { + return + } + + owns, err := user.OwnsAlbum(db, &separateAlbum) + assert.NoError(t, err) + assert.False(t, owns) +} + +func TestUserFavoriteMedia(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user, err := models.RegisterUser(db, "user1", nil, false) + assert.NoError(t, err) + + rootAlbum := models.Album{ + Title: "root", + Path: "/photos", + } + + assert.NoError(t, db.Save(&rootAlbum).Error) + assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum)) + + media := models.Media{ + Title: "pic1", + Path: "/photos/pic1", + AlbumID: rootAlbum.ID, + } + + assert.NoError(t, db.Save(&media).Error) + + // test that it starts out being false + favourite, err := dataloader.NewUserFavoriteLoader(db).Load(&models.UserMediaData{ + UserID: user.ID, + MediaID: media.ID, + }) + + assert.NoError(t, err) + assert.False(t, favourite) + + favMedia, err := user.FavoriteMedia(db, media.ID, true) + assert.NoError(t, err) + assert.NotNil(t, favMedia) + + // test that it is now true + favourite, err = dataloader.NewUserFavoriteLoader(db).Load(&models.UserMediaData{ + UserID: user.ID, + MediaID: media.ID, + }) + + assert.NoError(t, err) + assert.True(t, favourite) +} diff --git a/api/graphql/models/utils.go b/api/graphql/models/utils.go new file mode 100644 index 0000000..38cb081 --- /dev/null +++ b/api/graphql/models/utils.go @@ -0,0 +1,46 @@ +package models + +import ( + "crypto/md5" + "encoding/hex" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func FormatSQL(tx *gorm.DB, order *Ordering, paginate *Pagination) *gorm.DB { + + if paginate != nil { + if paginate.Limit != nil { + tx.Limit(*paginate.Limit) + } + + if paginate.Offset != nil { + tx.Offset(*paginate.Offset) + } + } + + if order != nil && order.OrderBy != nil { + desc := false + if order.OrderDirection != nil && order.OrderDirection.IsValid() { + if *order.OrderDirection == OrderDirectionDesc { + desc = true + } + } + + tx.Order(clause.OrderByColumn{ + Column: clause.Column{ + Name: *order.OrderBy, + }, + Desc: desc, + }) + } + + return tx +} + +// MD5Hash hashes value to a 32 length digest, the result is the same as the MYSQL function md5() +func MD5Hash(value string) string { + hash := md5.Sum([]byte(value)) + return hex.EncodeToString(hash[:]) +} diff --git a/api/graphql/models/utils_test.go b/api/graphql/models/utils_test.go new file mode 100644 index 0000000..99c3976 --- /dev/null +++ b/api/graphql/models/utils_test.go @@ -0,0 +1,12 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/stretchr/testify/assert" +) + +func TestMD5Hash(t *testing.T) { + assert.Equal(t, "5eb63bbbe01eeed093cb22bb8f5acdc3", models.MD5Hash("hello world")) +} diff --git a/api/graphql/models/video_metadata.go b/api/graphql/models/video_metadata.go new file mode 100644 index 0000000..16b81c9 --- /dev/null +++ b/api/graphql/models/video_metadata.go @@ -0,0 +1,17 @@ +package models + +type VideoMetadata struct { + Model + Width int `gorm:"not null"` + Height int `gorm:"not null"` + Duration float64 `gorm:"not null"` + Codec *string + Framerate *float64 + Bitrate *string + ColorProfile *string + Audio *string +} + +func (metadata *VideoMetadata) Media() *Media { + panic("not implemented") +} diff --git a/api/graphql/notification/Notification.go b/api/graphql/notification/Notification.go new file mode 100644 index 0000000..457b452 --- /dev/null +++ b/api/graphql/notification/Notification.go @@ -0,0 +1,83 @@ +package notification + +import ( + "errors" + "log" + "sync" + + "github.com/photoview/photoview/api/graphql/models" +) + +type NotificationChannel = chan<- *models.Notification + +type NotificationListener struct { + listenerID int + user models.User + channel NotificationChannel +} + +func NewListener(user models.User, channel NotificationChannel) *NotificationListener { + nextNotificationId++ + return &NotificationListener{ + listenerID: nextNotificationId, + user: user, + channel: channel, + } +} + +var notificationListeners []*NotificationListener = make([]*NotificationListener, 0) +var nextNotificationId = 0 +var notificationLock = &sync.Mutex{} + +func RegisterListener(user *models.User, channel NotificationChannel) int { + log.Println("Registering notification listener") + + notificationLock.Lock() + defer notificationLock.Unlock() + + notificationListeners = append(notificationListeners, NewListener(*user, channel)) + return nextNotificationId +} + +func DeregisterListener(listenerID int) error { + + notificationLock.Lock() + defer notificationLock.Unlock() + + for i, listener := range notificationListeners { + + log.Println("Deregistering notification listener") + + if listener.listenerID == listenerID { + + if len(notificationListeners) > 1 { + lastIndex := len(notificationListeners) - 1 + lastListener := notificationListeners[lastIndex] + notificationListeners[i] = lastListener + notificationListeners[lastIndex] = nil + notificationListeners = notificationListeners[:lastIndex] + } else { + notificationListeners = make([]*NotificationListener, 0) + } + + return nil + } + } + + return errors.New("ListenerID not found, while trying to deregister it") +} + +func BroadcastNotification(notification *models.Notification) { + + if notification == nil { + return + } + + notificationLock.Lock() + defer notificationLock.Unlock() + + for _, listener := range notificationListeners { + listener.channel <- notification + } + +} diff --git a/api/graphql/resolvers/album.go b/api/graphql/resolvers/album.go new file mode 100644 index 0000000..2551668 --- /dev/null +++ b/api/graphql/resolvers/album.go @@ -0,0 +1,167 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "errors" + "fmt" + + api "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "gorm.io/gorm" +) + +// Media is the resolver for the media field. +func (r *albumResolver) Media(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination, onlyFavorites *bool) ([]*models.Media, error) { + db := r.DB(ctx) + + query := db. + Where("media.album_id = ?", obj.ID). + Where("media.id IN (?)", db.Model(&models.MediaURL{}). + Select("media_urls.media_id"). + Where("media_urls.media_id = media.id")) + + if onlyFavorites != nil && *onlyFavorites == true { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("cannot get favorite media without being authorized") + } + + favoriteQuery := db.Model(&models.UserMediaData{ + UserID: user.ID, + }).Where("user_media_data.media_id = media.id").Where("user_media_data.favorite = true") + + query = query.Where("EXISTS (?)", favoriteQuery) + } + + query = models.FormatSQL(query, order, paginate) + + var media []*models.Media + if err := query.Find(&media).Error; err != nil { + return nil, err + } + + return media, nil +} + +// SubAlbums is the resolver for the subAlbums field. +func (r *albumResolver) SubAlbums(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination) ([]*models.Album, error) { + var albums []*models.Album + + query := r.DB(ctx).Where("parent_album_id = ?", obj.ID) + query = models.FormatSQL(query, order, paginate) + + if err := query.Find(&albums).Error; err != nil { + return nil, err + } + + return albums, nil +} + +// Owner is the resolver for the owner field. +func (r *albumResolver) Owner(ctx context.Context, obj *models.Album) (*models.User, error) { + panic("not implemented") +} + +// Thumbnail is the resolver for the thumbnail field. +func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error) { + return obj.Thumbnail(r.DB(ctx)) +} + +// Path is the resolver for the path field. +func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.Album, error) { + user := auth.UserFromContext(ctx) + if user == nil { + empty := make([]*models.Album, 0) + return empty, nil + } + + return actions.AlbumPath(r.DB(ctx), user, obj) +} + +// Shares is the resolver for the shares field. +func (r *albumResolver) Shares(ctx context.Context, obj *models.Album) ([]*models.ShareToken, error) { + var shareTokens []*models.ShareToken + if err := r.DB(ctx).Where("album_id = ?", obj.ID).Find(&shareTokens).Error; err != nil { + return nil, err + } + + return shareTokens, nil +} + +// Takes album_id, resets album.cover_id to 0 (null) +func (r *mutationResolver) ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + return actions.ResetAlbumCover(r.DB(ctx), user, albumID) +} + +// SetAlbumCover is the resolver for the setAlbumCover field. +func (r *mutationResolver) SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + return actions.SetAlbumCover(r.DB(ctx), user, coverID) +} + +// MyAlbums is the resolver for the myAlbums field. +func (r *queryResolver) MyAlbums(ctx context.Context, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.MyAlbums(r.DB(ctx), user, order, paginate, onlyRoot, showEmpty, onlyWithFavorites) +} + +// Album is the resolver for the album field. +func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) { + db := r.DB(ctx) + if tokenCredentials != nil { + + shareToken, err := r.ShareToken(ctx, *tokenCredentials) + if err != nil { + return nil, err + } + + if shareToken.Album != nil { + if *shareToken.AlbumID == id { + return shareToken.Album, nil + } + + subAlbum, err := shareToken.Album.GetChildren(db, func(query *gorm.DB) *gorm.DB { + return query.Where("sub_albums.id = ?", id) + }) + if err != nil { + return nil, fmt.Errorf("find sub album of share token (%s): %w", tokenCredentials.Token, err) + } + + if len(subAlbum) > 0 { + return subAlbum[0], nil + } + } + } + + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.Album(db, user, id) +} + +// Album returns api.AlbumResolver implementation. +func (r *Resolver) Album() api.AlbumResolver { return &albumResolver{r} } + +type albumResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/album.graphql b/api/graphql/resolvers/album.graphql new file mode 100644 index 0000000..bb43c04 --- /dev/null +++ b/api/graphql/resolvers/album.graphql @@ -0,0 +1,60 @@ +type Album { + id: ID! + title: String! + + "The media inside this album" + media( + order: Ordering, + paginate: Pagination + "Return only the favorited media" + onlyFavorites: Boolean + ): [Media!]! + + "The albums contained in this album" + subAlbums( + order: Ordering, + paginate: Pagination + ): [Album!]! + + "The album which contains this album" + parentAlbum: Album + "The user who owns this album" + owner: User! + "The path on the filesystem of the server, where this album is located" + filePath: String! + "An image in this album used for previewing this album" + thumbnail: Media + "A breadcrumb list of all parent albums down to this one" + path: [Album!]! + + "A list of share tokens pointing to this album, owned by the logged in user" + shares: [ShareToken!]! +} + +extend type Query { + "List of albums owned by the logged in user." + myAlbums( + order: Ordering, + paginate: Pagination + "Return only albums from the root directory of the user" + onlyRoot: Boolean + "Return also albums with no media directly in them" + showEmpty: Boolean + "Show only albums having favorites" + onlyWithFavorites: Boolean + ): [Album!]! @isAuthorized + + """ + Get album by id, user must own the album or be admin + If valid tokenCredentials are provided, the album may be retrived without further authentication + """ + album(id: ID!, tokenCredentials: ShareTokenCredentials): Album! +} + +extend type Mutation { + "Reset the assigned cover photo for an album" + resetAlbumCover(albumID: ID!): Album! @isAuthorized + + "Assign a cover photo to an album" + setAlbumCover(coverID: ID!): Album! @isAuthorized +} diff --git a/api/graphql/resolvers/faces.go b/api/graphql/resolvers/faces.go new file mode 100644 index 0000000..63e705a --- /dev/null +++ b/api/graphql/resolvers/faces.go @@ -0,0 +1,442 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "errors" + + api "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" + "gorm.io/gorm" +) + +// ImageFaces is the resolver for the imageFaces field. +func (r *faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup, paginate *models.Pagination) ([]*models.ImageFace, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + if err := user.FillAlbums(db); err != nil { + return nil, err + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + query := db. + Joins("Media"). + Where(faceGroupIDIsQuestion, obj.ID). + Where("album_id IN (?)", userAlbumIDs) + + query = models.FormatSQL(query, nil, paginate) + + var imageFaces []*models.ImageFace + if err := query.Find(&imageFaces).Error; err != nil { + return nil, err + } + + return imageFaces, nil +} + +// ImageFaceCount is the resolver for the imageFaceCount field. +func (r *faceGroupResolver) ImageFaceCount(ctx context.Context, obj *models.FaceGroup) (int, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return -1, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return -1, ErrFaceDetectorNotInitialized + } + + if err := user.FillAlbums(db); err != nil { + return -1, err + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + query := db. + Model(&models.ImageFace{}). + Joins("Media"). + Where(faceGroupIDIsQuestion, obj.ID). + Where("album_id IN (?)", userAlbumIDs) + + var count int64 + if err := query.Count(&count).Error; err != nil { + return -1, err + } + + return int(count), nil +} + +// Media is the resolver for the media field. +func (r *imageFaceResolver) Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error) { + if err := obj.FillMedia(r.DB(ctx)); err != nil { + return nil, err + } + + return &obj.Media, nil +} + +// FaceGroup is the resolver for the faceGroup field. +func (r *imageFaceResolver) FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error) { + if obj.FaceGroup != nil { + return obj.FaceGroup, nil + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + var faceGroup models.FaceGroup + if err := r.DB(ctx).Model(&obj).Association("FaceGroup").Find(&faceGroup); err != nil { + return nil, err + } + + obj.FaceGroup = &faceGroup + + return &faceGroup, nil +} + +// SetFaceGroupLabel is the resolver for the setFaceGroupLabel field. +func (r *mutationResolver) SetFaceGroupLabel(ctx context.Context, faceGroupID int, label *string) (*models.FaceGroup, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + faceGroup, err := userOwnedFaceGroup(db, user, faceGroupID) + if err != nil { + return nil, err + } + + if err := db.Model(faceGroup).Update("label", label).Error; err != nil { + return nil, err + } + + return faceGroup, nil +} + +// CombineFaceGroups is the resolver for the combineFaceGroups field. +func (r *mutationResolver) CombineFaceGroups(ctx context.Context, destinationFaceGroupID int, sourceFaceGroupIDs []int) (*models.FaceGroup, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + if len(sourceFaceGroupIDs) < 1 { + return nil, errors.New("at least one source face group ID is required") + } + + destinationFaceGroup, err := userOwnedFaceGroup(db, user, destinationFaceGroupID) + if err != nil { + return nil, err + } + + var sourceFaceGroups []*models.FaceGroup + + // Validate the source groups before beginning the merge + for _, sourceID := range sourceFaceGroupIDs { + if sourceID == destinationFaceGroup.ID { + return nil, errors.New("source face groups cannot include the destination face group") + } + + // Ensure the user owns at least one picture with each source + sourceFaceGroup, err := userOwnedFaceGroup(db, user, sourceID) + if err != nil { + return nil, err + } + + sourceFaceGroups = append(sourceFaceGroups, sourceFaceGroup) + } + + // Perform the merge + updateError := db.Transaction(func(tx *gorm.DB) error { + + if err := tx. + Model(&models.ImageFace{}). + Where(faceGroupIDsInQuestion, sourceFaceGroupIDs). + Update("face_group_id", destinationFaceGroup.ID).Error; err != nil { + return err + } + + // delete the source face groups + if err := deleteFaceGroups(sourceFaceGroups, tx); err != nil { + return err + } + subQuery := tx.Model(&models.ImageFace{}). + Select("MIN(id)"). + Where("face_group_id = ?", destinationFaceGroup.ID). + Group("media_id") + + err := tx.Where("face_group_id = ?", destinationFaceGroup.ID). + Where("id NOT IN (?)", subQuery). + Delete(&models.ImageFace{}). + Error + if err != nil { + return err + } + return nil + }) + + if updateError != nil { + return nil, updateError + } + + face_detection.GlobalFaceDetector.MergeImageFaces(sourceFaceGroupIDs, int32(destinationFaceGroupID)) + + return destinationFaceGroup, nil +} + +// MoveImageFaces is the resolver for the moveImageFaces field. +func (r *mutationResolver) MoveImageFaces(ctx context.Context, imageFaceIDs []int, destinationFaceGroupID int) (*models.FaceGroup, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + userOwnedImageFaceIDs := make([]int, 0) + var destFaceGroup *models.FaceGroup + + transErr := db.Transaction(func(tx *gorm.DB) error { + + var err error + destFaceGroup, err = userOwnedFaceGroup(tx, user, destinationFaceGroupID) + if err != nil { + return err + } + + userOwnedImageFaces, err := getUserOwnedImageFaces(tx, user, imageFaceIDs) + if err != nil { + return err + } + + for _, imageFace := range userOwnedImageFaces { + userOwnedImageFaceIDs = append(userOwnedImageFaceIDs, imageFace.ID) + } + + var sourceFaceGroups []*models.FaceGroup + if err := tx. + Joins("LEFT JOIN image_faces ON image_faces.face_group_id = face_groups.id"). + Where(imageFacesIDInQuestion, userOwnedImageFaceIDs). + Find(&sourceFaceGroups).Error; err != nil { + return err + } + + if err := tx. + Model(&models.ImageFace{}). + Where("id IN (?)", userOwnedImageFaceIDs). + Update("face_group_id", destFaceGroup.ID).Error; err != nil { + return err + } + + // delete face groups if they have become empty + if err := deleteEmptyFaceGroups(sourceFaceGroups, tx); err != nil { + return err + } + + return nil + }) + + if transErr != nil { + return nil, transErr + } + + face_detection.GlobalFaceDetector.MergeImageFaces(userOwnedImageFaceIDs, int32(destFaceGroup.ID)) + + return destFaceGroup, nil +} + +// RecognizeUnlabeledFaces is the resolver for the recognizeUnlabeledFaces field. +func (r *mutationResolver) RecognizeUnlabeledFaces(ctx context.Context) ([]*models.ImageFace, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + var updatedImageFaces []*models.ImageFace + + transactionError := db.Transaction(func(tx *gorm.DB) error { + var err error + updatedImageFaces, err = face_detection.GlobalFaceDetector.RecognizeUnlabeledFaces(tx, user) + + return err + }) + + if transactionError != nil { + return nil, transactionError + } + + return updatedImageFaces, nil +} + +// DetachImageFaces is the resolver for the detachImageFaces field. +func (r *mutationResolver) DetachImageFaces(ctx context.Context, imageFaceIDs []int) (*models.FaceGroup, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + userOwnedImageFaceIDs := make([]int, 0) + newFaceGroup := models.FaceGroup{} + + transactionError := db.Transaction(func(tx *gorm.DB) error { + + userOwnedImageFaces, err := getUserOwnedImageFaces(tx, user, imageFaceIDs) + if err != nil { + return err + } + + for _, imageFace := range userOwnedImageFaces { + userOwnedImageFaceIDs = append(userOwnedImageFaceIDs, imageFace.ID) + } + + if err := tx.Save(&newFaceGroup).Error; err != nil { + return err + } + + if err := tx. + Model(&models.ImageFace{}). + Where("id IN (?)", userOwnedImageFaceIDs). + Update("face_group_id", newFaceGroup.ID).Error; err != nil { + return err + } + + return nil + }) + + if transactionError != nil { + return nil, transactionError + } + + face_detection.GlobalFaceDetector.MergeImageFaces(userOwnedImageFaceIDs, int32(newFaceGroup.ID)) + + return &newFaceGroup, nil +} + +// MyFaceGroups is the resolver for the myFaceGroups field. +func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + if err := user.FillAlbums(db); err != nil { + return nil, err + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + faceGroupQuery := db. + Joins("JOIN image_faces ON image_faces.face_group_id = face_groups.id"). + Where("image_faces.media_id IN (?)", + db.Select("media.id").Table("media").Where(mediaAlbumIDInQuestion, userAlbumIDs)). + Group("image_faces.face_group_id"). + Group("face_groups.id"). + Order("CASE WHEN label IS NULL THEN 1 ELSE 0 END"). + Order("COUNT(image_faces.id) DESC") + + faceGroupQuery = models.FormatSQL(faceGroupQuery, nil, paginate) + + var faceGroups []*models.FaceGroup + if err := faceGroupQuery.Find(&faceGroups).Error; err != nil { + return nil, err + } + + return faceGroups, nil +} + +// FaceGroup is the resolver for the faceGroup field. +func (r *queryResolver) FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if face_detection.GlobalFaceDetector == nil { + return nil, ErrFaceDetectorNotInitialized + } + + if err := user.FillAlbums(db); err != nil { + return nil, err + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + faceGroupQuery := db. + Joins("LEFT JOIN image_faces ON image_faces.face_group_id = face_groups.id"). + Joins("LEFT JOIN media ON image_faces.media_id = media.id"). + Where("face_groups.id = ?", id). + Where(mediaAlbumIDInQuestion, userAlbumIDs) + + var faceGroup models.FaceGroup + if err := faceGroupQuery.Find(&faceGroup).Error; err != nil { + return nil, err + } + + return &faceGroup, nil +} + +// FaceGroup returns api.FaceGroupResolver implementation. +func (r *Resolver) FaceGroup() api.FaceGroupResolver { return &faceGroupResolver{r} } + +// ImageFace returns api.ImageFaceResolver implementation. +func (r *Resolver) ImageFace() api.ImageFaceResolver { return &imageFaceResolver{r} } + +type faceGroupResolver struct{ *Resolver } +type imageFaceResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/faces.graphql b/api/graphql/resolvers/faces.graphql new file mode 100644 index 0000000..916ac98 --- /dev/null +++ b/api/graphql/resolvers/faces.graphql @@ -0,0 +1,53 @@ +"A bounding box of where a face is present on an image. The values map from 0 to 1 as a fraction of the image width/height" +type FaceRectangle { + minX: Float! + maxX: Float! + minY: Float! + maxY: Float! +} + +"A collection of faces of a particular person" +type FaceGroup { + id: ID! + "The name of the person" + label: String + imageFaces(paginate: Pagination): [ImageFace!]! + "The total number of images in this collection" + imageFaceCount: Int! +} + +"A single face on a particular image" +type ImageFace { + id: ID! + "A reference to the image the face appears on" + media: Media! + "A bounding box of where on the image the face is present" + rectangle: FaceRectangle! + "The `FaceGroup` that contains this `ImageFace`" + faceGroup: FaceGroup! +} + +extend type Query { + "Get a list of `FaceGroup`s for the logged in user" + myFaceGroups(paginate: Pagination): [FaceGroup!]! @isAuthorized + + "Get a particular `FaceGroup` specified by its ID" + faceGroup(id: ID!): FaceGroup! @isAuthorized +} + +extend type Mutation { + "Assign a label to a face group, set label to null to remove the current one" + setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized + + "Merge two face groups into a single one, all ImageFaces from source will be moved to destination" + combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupIDs: [ID!]!): FaceGroup! @isAuthorized + + "Move a list of ImageFaces to another face group" + moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! @isAuthorized + + "Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match" + recognizeUnlabeledFaces: [ImageFace!]! @isAuthorized + + "Move a list of ImageFaces to a new face group" + detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized +} diff --git a/api/graphql/resolvers/faces.util.go b/api/graphql/resolvers/faces.util.go new file mode 100644 index 0000000..5450731 --- /dev/null +++ b/api/graphql/resolvers/faces.util.go @@ -0,0 +1,106 @@ +package resolvers + +import ( + "errors" + "fmt" + + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +const faceGroupIDIsQuestion = "face_group_id = ?" +const faceGroupIDsInQuestion = "face_group_id IN (?)" +const mediaAlbumIDInQuestion = "media.album_id IN (?)" +const imageFacesIDInQuestion = "image_faces.id IN (?)" + +var ErrFaceDetectorNotInitialized = errors.New("face detector not initialized") + +func userOwnedFaceGroup(db *gorm.DB, user *models.User, faceGroupID int) (*models.FaceGroup, error) { + if user.Admin { + var faceGroup models.FaceGroup + if err := db.Where("id = ?", faceGroupID).Find(&faceGroup).Error; err != nil { + return nil, err + } + + return &faceGroup, nil + } + + if err := user.FillAlbums(db); err != nil { + return nil, err + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + // Verify that user owns at least one of the images in the face group + imageFaceQuery := db. + Select("image_faces.id"). + Table("image_faces"). + Joins("JOIN media ON media.id = image_faces.media_id"). + Where(mediaAlbumIDInQuestion, userAlbumIDs) + + faceGroupQuery := db. + Model(&models.FaceGroup{}). + Joins("JOIN image_faces ON face_groups.id = image_faces.face_group_id"). + Where("face_groups.id = ?", faceGroupID). + Where(imageFacesIDInQuestion, imageFaceQuery) + + var faceGroup models.FaceGroup + if err := faceGroupQuery.Find(&faceGroup).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("face group does not exist or is not owned by the user: %w", err) + } + return nil, err + } + + return &faceGroup, nil +} + +func getUserOwnedImageFaces(tx *gorm.DB, user *models.User, imageFaceIDs []int) ([]*models.ImageFace, error) { + if err := user.FillAlbums(tx); err != nil { + return nil, err + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + var userOwnedImageFaces []*models.ImageFace + if err := tx. + Joins("JOIN media ON media.id = image_faces.media_id"). + Where(mediaAlbumIDInQuestion, userAlbumIDs). + Where(imageFacesIDInQuestion, imageFaceIDs). + Find(&userOwnedImageFaces).Error; err != nil { + return nil, err + } + + return userOwnedImageFaces, nil +} + +func deleteEmptyFaceGroups(sourceFaceGroups []*models.FaceGroup, tx *gorm.DB) error { + for _, faceGroup := range sourceFaceGroups { + var count int64 + if err := tx.Model(&models.ImageFace{}).Where(faceGroupIDIsQuestion, faceGroup.ID).Count(&count).Error; err != nil { + return err + } + + if count == 0 { + if err := tx.Delete(&faceGroup).Error; err != nil { + return err + } + } + } + return nil +} + +func deleteFaceGroups(sourceFaceGroups []*models.FaceGroup, tx *gorm.DB) error { + for _, faceGroup := range sourceFaceGroups { + if err := tx.Delete(&faceGroup).Error; err != nil { + return fmt.Errorf("Delete FaceGroup(%d) error: %w", faceGroup.ID, err) + } + } + return nil +} diff --git a/api/graphql/resolvers/faces_test.go b/api/graphql/resolvers/faces_test.go new file mode 100644 index 0000000..1f94b10 --- /dev/null +++ b/api/graphql/resolvers/faces_test.go @@ -0,0 +1,106 @@ +package resolvers + +import ( + "context" + "testing" + + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" + "github.com/photoview/photoview/api/test_utils" +) + +func TestCombineFaceGroups(t *testing.T) { + test_utils.FilesystemTest(t) + db := test_utils.DatabaseTest(t) + face_detection.InitializeFaceDetector(db) + pass := "1234" + user, err := models.RegisterUser(db, "test_user", &pass, true) + if err != nil { + t.Fatal("register user error:", err) + } + db.AutoMigrate(&models.ImageFace{}, &models.FaceGroup{}, &models.Media{}, &models.Album{}) + tests := []struct { + name string + dest int + src []int + }{ + { + name: "merge multiple combinations with duplicates", + dest: 1, + src: []int{2, 3}, + }, + { + name: "merge two combinations with duplicates", + dest: 1, + src: []int{2}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + db.Exec("DELETE FROM image_faces") + db.Exec("DELETE FROM face_groups") + db.Exec("DELETE FROM media") + db.Exec("DELETE FROM albums") + + testAlbum := models.Album{Title: "Test Album"} + if err := db.Create(&testAlbum).Error; err != nil { + t.Fatal(err) + } + + testMedia := []models.Media{ + {Model: models.Model{ID: 1}, Path: "test1", AlbumID: testAlbum.ID}, + {Model: models.Model{ID: 2}, Path: "test2", AlbumID: testAlbum.ID}, + {Model: models.Model{ID: 3}, Path: "test3", AlbumID: testAlbum.ID}, + {Model: models.Model{ID: 4}, Path: "test4", AlbumID: testAlbum.ID}, + } + if err := db.Create(&testMedia).Error; err != nil { + t.Fatal(err) + } + + testFaceGroup := []models.FaceGroup{ + {Model: models.Model{ID: 1}}, + {Model: models.Model{ID: 2}}, + {Model: models.Model{ID: 3}}, + {Model: models.Model{ID: 4}}, + } + if err := db.Create(&testFaceGroup).Error; err != nil { + t.Fatal(err) + } + + testDataList := []models.ImageFace{ + {FaceGroupID: 1, MediaID: 1}, + {FaceGroupID: 1, MediaID: 2}, + {FaceGroupID: 1, MediaID: 3}, + {FaceGroupID: 2, MediaID: 3}, + {FaceGroupID: 2, MediaID: 4}, + {FaceGroupID: 3, MediaID: 4}, + {FaceGroupID: 3, MediaID: 1}, + } + if err := db.Create(&testDataList).Error; err != nil { + t.Fatal(err) + } + + r := &mutationResolver{ + Resolver: &Resolver{ + database: db, + }, + } + ctx := auth.AddUserToContext(context.Background(), user) + + combineFace, err := r.CombineFaceGroups(ctx, tt.dest, tt.src) + if err != nil { + t.Fatal("test CombineFaceGroups err:", err) + } + + m := make(map[int]struct{}) + for _, imageface := range combineFace.ImageFaces { + if _, ok := m[imageface.MediaID]; ok { + t.Fatal("filtering failed at", imageface.MediaID) + } + m[imageface.MediaID] = struct{}{} + } + }) + } +} diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go new file mode 100644 index 0000000..3fa376d --- /dev/null +++ b/api/graphql/resolvers/media.go @@ -0,0 +1,236 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "fmt" + + "github.com/photoview/photoview/api/dataloader" + api "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/scanner/face_detection" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// Thumbnail is the resolver for the thumbnail field. +func (r *mediaResolver) Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error) { + return dataloader.For(ctx).MediaThumbnail.Load(obj.ID) +} + +// HighRes is the resolver for the highRes field. +func (r *mediaResolver) HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error) { + if obj.Type != models.MediaTypePhoto { + return nil, nil + } + + return dataloader.For(ctx).MediaHighres.Load(obj.ID) +} + +// VideoWeb is the resolver for the videoWeb field. +func (r *mediaResolver) VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error) { + if obj.Type != models.MediaTypeVideo { + return nil, nil + } + + return dataloader.For(ctx).MediaVideoWeb.Load(obj.ID) +} + +// Album is the resolver for the album field. +func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.Album, error) { + var album models.Album + err := r.DB(ctx).Find(&album, obj.AlbumID).Error + if err != nil { + return nil, err + } + return &album, nil +} + +// Exif is the resolver for the exif field. +func (r *mediaResolver) Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, error) { + if obj.Exif != nil { + return obj.Exif, nil + } + + var exif models.MediaEXIF + if err := r.DB(ctx).Model(obj).Association("Exif").Find(&exif); err != nil { + return nil, err + } + + return &exif, nil +} + +// Favorite is the resolver for the favorite field. +func (r *mediaResolver) Favorite(ctx context.Context, obj *models.Media) (bool, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return false, auth.ErrUnauthorized + } + + return dataloader.For(ctx).UserMediaFavorite.Load(&models.UserMediaData{ + UserID: user.ID, + MediaID: obj.ID, + }) +} + +// Type is the resolver for the type field. +func (r *mediaResolver) Type(ctx context.Context, obj *models.Media) (models.MediaType, error) { + formattedType := models.MediaType(cases.Title(language.Und).String(string(obj.Type))) + return formattedType, nil +} + +// Shares is the resolver for the shares field. +func (r *mediaResolver) Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error) { + var shareTokens []*models.ShareToken + if err := r.DB(ctx).Where("media_id = ?", obj.ID).Find(&shareTokens).Error; err != nil { + return nil, fmt.Errorf("get shares for media (%s): %w", obj.Path, err) + } + + return shareTokens, nil +} + +// Downloads is the resolver for the downloads field. +func (r *mediaResolver) Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error) { + var mediaUrls []*models.MediaURL + if err := r.DB(ctx).Where("media_id = ?", obj.ID).Find(&mediaUrls).Error; err != nil { + return nil, fmt.Errorf("get downloads for media (%s): %w", obj.Path, err) + } + + downloads := make([]*models.MediaDownload, 0) + + for _, url := range mediaUrls { + + var title string + switch { + case url.Purpose == models.MediaOriginal: + title = "Original" + case url.Purpose == models.PhotoThumbnail: + title = "Small" + case url.Purpose == models.PhotoHighRes: + title = "Large" + case url.Purpose == models.VideoThumbnail: + title = "Video thumbnail" + case url.Purpose == models.VideoWeb: + title = "Web optimized video" + } + + downloads = append(downloads, &models.MediaDownload{ + Title: title, + MediaURL: url, + }) + } + + return downloads, nil +} + +// Faces is the resolver for the faces field. +func (r *mediaResolver) Faces(ctx context.Context, obj *models.Media) ([]*models.ImageFace, error) { + if face_detection.GlobalFaceDetector == nil { + return []*models.ImageFace{}, nil + } + + if obj.Faces != nil { + return obj.Faces, nil + } + + var faces []*models.ImageFace + if err := r.DB(ctx).Model(obj).Association("Faces").Find(&faces); err != nil { + return nil, err + } + + return faces, nil +} + +// FavoriteMedia is the resolver for the favoriteMedia field. +func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return user.FavoriteMedia(r.DB(ctx), mediaID, favorite) +} + +// MyMedia is the resolver for the myMedia field. +func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, fmt.Errorf("unauthorized") + } + + return actions.MyMedia(r.DB(ctx), user, order, paginate) +} + +// Media is the resolver for the media field. +func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) { + db := r.DB(ctx) + if tokenCredentials != nil { + + shareToken, err := r.ShareToken(ctx, *tokenCredentials) + if err != nil { + return nil, err + } + + if *shareToken.MediaID == id { + return shareToken.Media, nil + } + } + + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + var media models.Media + + err := db. + Joins("Album"). + Where("media.id = ?", id). + Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = media.album_id AND user_albums.user_id = ?)", + user.ID). + Where("media.id IN (?)", db.Model(&models.MediaURL{}).Select("media_id").Where("media_urls.media_id = media.id")). + First(&media).Error + + if err != nil { + return nil, fmt.Errorf("could not get media by media_id and user_id from database: %w", err) + } + + return &media, nil +} + +// MediaList is the resolver for the mediaList field. +func (r *queryResolver) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + if len(ids) == 0 { + return nil, fmt.Errorf("no ids provided") + } + + var media []*models.Media + err := db.Model(&media). + Joins("LEFT JOIN user_albums ON user_albums.album_id = media.album_id"). + Where("media.id IN ?", ids). + Where("user_albums.user_id = ?", user.ID). + Find(&media).Error + + if err != nil { + return nil, fmt.Errorf("could not get media list by media_id and user_id from database: %w", err) + } + + return media, nil +} + +// Media returns api.MediaResolver implementation. +func (r *Resolver) Media() api.MediaResolver { return &mediaResolver{r} } + +type mediaResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/media.graphql b/api/graphql/resolvers/media.graphql new file mode 100644 index 0000000..bb44fab --- /dev/null +++ b/api/graphql/resolvers/media.graphql @@ -0,0 +1,123 @@ +type MediaURL { + "URL for previewing the image" + url: String! + "Width of the image in pixels" + width: Int! + "Height of the image in pixels" + height: Int! + "The file size of the resource in bytes" + fileSize: Int! +} + +type MediaDownload { + "A description of the role of the media file" + title: String! + mediaUrl: MediaURL! +} + +enum MediaType { + Photo + Video +} + +type Coordinates { + "GPS latitude in degrees" + latitude: Float! + "GPS longitude in degrees" + longitude: Float! +} + + +"EXIF metadata from the camera" +type MediaEXIF { + id: ID! + media: Media! + "The description of the image" + description: String + "The model name of the camera" + camera: String + "The maker of the camera" + maker: String + "The name of the lens" + lens: String + "The date when the photo is shot" + dateShot: String + "The exposure time of the image" + exposure: Float + "The aperature stops of the image" + aperture: Float + "The ISO setting of the image" + iso: Int + "The focal length of the lens, when the image was taken" + focalLength: Float + "A formatted description of the flash settings, when the image was taken" + flash: Int + "An index describing the mode for adjusting the exposure of the image" + exposureProgram: Int + "GPS coordinates of where the image was taken" + coordinates: Coordinates +} + +"Metadata specific to video media" +type VideoMetadata { + id: ID! + media: Media! + width: Int! + height: Int! + duration: Float! + codec: String + framerate: Float + bitrate: String + colorProfile: String + audio: String +} + +type Media { + id: ID! + title: String! + "Local filepath for the media" + path: String! + "URL to display the media in a smaller resolution" + thumbnail: MediaURL + "URL to display the photo in full resolution, will be null for videos" + highRes: MediaURL + "URL to get the video in a web format that can be played in the browser, will be null for photos" + videoWeb: MediaURL + "The album that holds the media" + album: Album! + exif: MediaEXIF + videoMetadata: VideoMetadata + favorite: Boolean! + type: MediaType! + "The date the image was shot or the date it was imported as a fallback" + date: Time! + "A short string that can be used to generate a blured version of the media, to show while the original is loading" + blurhash: String + + "A list of share tokens pointing to this media, owned byt the logged in user" + shares: [ShareToken!]! + "A list of different versions of files for this media that can be downloaded by the user" + downloads: [MediaDownload!]! + + "A list of faces present on the image" + faces: [ImageFace!]! +} + +extend type Query { + "List of media owned by the logged in user" + myMedia(order: Ordering, paginate: Pagination): [Media!]! @isAuthorized + + """ + Get media by id, user must own the media or be admin. + If valid tokenCredentials are provided, the media may be retrived without further authentication + """ + media(id: ID!, tokenCredentials: ShareTokenCredentials): Media! + + "Get a list of media by their ids, user must own the media or be admin" + mediaList(ids: [ID!]!): [Media!]! +} + +extend type Mutation { + "Mark or unmark a media as being a favorite" + favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized +} diff --git a/api/graphql/resolvers/media_geo_json.go b/api/graphql/resolvers/media_geo_json.go new file mode 100644 index 0000000..08d779f --- /dev/null +++ b/api/graphql/resolvers/media_geo_json.go @@ -0,0 +1,81 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "os" + "path" + + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/utils" +) + +// MyMediaGeoJSON is the resolver for the myMediaGeoJson field. +func (r *queryResolver) MyMediaGeoJSON(ctx context.Context) (any, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + var media []*geoMedia + + err := r.DB(ctx).Table("media"). + Select("media.id AS media_id, media.title AS media_title, "+ + "media_urls.media_name AS thumbnail_name, media_urls.width AS thumbnail_width, "+ + "media_urls.height AS thumbnail_height, media_exif.gps_latitude AS latitude, "+ + "media_exif.gps_longitude AS longitude"). + Joins("INNER JOIN media_exif ON media.exif_id = media_exif.id"). + Joins("INNER JOIN media_urls ON media.id = media_urls.media_id"). + Joins("INNER JOIN user_albums ON media.album_id = user_albums.album_id"). + Where("media_exif.gps_latitude IS NOT NULL"). + Where("media_exif.gps_longitude IS NOT NULL"). + Where("media_urls.purpose = 'thumbnail'"). + Where("user_albums.user_id = ?", user.ID). + Scan(&media).Error + + if err != nil { + return nil, err + } + + features := make([]geoJSONFeature, 0) + + for _, item := range media { + geoPoint := makeGeoJSONFeatureGeometryPoint(item.Latitude, item.Longitude) + + thumbnailURL := utils.ApiEndpointUrl() + thumbnailURL.Path = path.Join(thumbnailURL.Path, "photo", item.ThumbnailName) + + properties := geoJSONMediaProperties{ + MediaID: item.MediaID, + MediaTitle: item.MediaTitle, + Thumbnail: struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + }{ + URL: thumbnailURL.String(), + Width: item.ThumbnailWidth, + Height: item.ThumbnailHeight, + }, + } + + features = append(features, makeGeoJSONFeature(properties, geoPoint)) + } + + featureCollection := makeGeoJSONFeatureCollection(features) + return featureCollection, nil +} + +// MapboxToken is the resolver for the mapboxToken field. +func (r *queryResolver) MapboxToken(ctx context.Context) (*string, error) { + mapboxTokenEnv := os.Getenv("MAPBOX_TOKEN") + if mapboxTokenEnv == "" { + return nil, nil + } + + return &mapboxTokenEnv, nil +} diff --git a/api/graphql/resolvers/media_geo_json.graphql b/api/graphql/resolvers/media_geo_json.graphql new file mode 100644 index 0000000..66a339b --- /dev/null +++ b/api/graphql/resolvers/media_geo_json.graphql @@ -0,0 +1,7 @@ +extend type Query { + "Get media owned by the logged in user, returned in GeoJson format" + myMediaGeoJson: Any! @isAuthorized + + "Get the mapbox api token, returns null if mapbox is not enabled" + mapboxToken: String +} diff --git a/api/graphql/resolvers/media_geo_json.util.go b/api/graphql/resolvers/media_geo_json.util.go new file mode 100644 index 0000000..2016d9b --- /dev/null +++ b/api/graphql/resolvers/media_geo_json.util.go @@ -0,0 +1,61 @@ +package resolvers + +type geoMedia struct { + MediaID int + MediaTitle string + ThumbnailName string + ThumbnailWidth int + ThumbnailHeight int + Latitude float64 + Longitude float64 +} + +type geoJSONFeatureCollection struct { + Type string `json:"type"` + Features []geoJSONFeature `json:"features"` +} + +type geoJSONFeature struct { + Type string `json:"type"` + Properties interface{} `json:"properties"` + Geometry geoJSONFeatureGeometry `json:"geometry"` +} + +type geoJSONMediaProperties struct { + MediaID int `json:"media_id"` + MediaTitle string `json:"media_title"` + Thumbnail struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + } `json:"thumbnail"` +} + +type geoJSONFeatureGeometry struct { + Type string `json:"type"` + Coordinates [2]float64 `json:"coordinates"` +} + +func makeGeoJSONFeatureCollection(features []geoJSONFeature) geoJSONFeatureCollection { + return geoJSONFeatureCollection{ + Type: "FeatureCollection", + Features: features, + } +} + +func makeGeoJSONFeature(properties interface{}, geometry geoJSONFeatureGeometry) geoJSONFeature { + return geoJSONFeature{ + Type: "Feature", + Properties: properties, + Geometry: geometry, + } +} + +func makeGeoJSONFeatureGeometryPoint(lat float64, long float64) geoJSONFeatureGeometry { + coordinates := [2]float64{long, lat} + + return geoJSONFeatureGeometry{ + Type: "Point", + Coordinates: coordinates, + } +} diff --git a/api/graphql/resolvers/notification.go b/api/graphql/resolvers/notification.go new file mode 100644 index 0000000..25e7eff --- /dev/null +++ b/api/graphql/resolvers/notification.go @@ -0,0 +1,39 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + + api "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/notification" +) + +// Notification is the resolver for the notification field. +func (r *subscriptionResolver) Notification(ctx context.Context) (<-chan *models.Notification, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + notificationChannel := make(chan *models.Notification, 1) + + listenerID := notification.RegisterListener(user, notificationChannel) + + go func() { + <-ctx.Done() + notification.DeregisterListener(listenerID) + }() + + return notificationChannel, nil +} + +// Subscription returns api.SubscriptionResolver implementation. +func (r *Resolver) Subscription() api.SubscriptionResolver { return &subscriptionResolver{r} } + +type subscriptionResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/notification.graphql b/api/graphql/resolvers/notification.graphql new file mode 100644 index 0000000..ffd044e --- /dev/null +++ b/api/graphql/resolvers/notification.graphql @@ -0,0 +1,31 @@ +type Notification { + "A key used to identify the notification, new notification updates with the same key, should replace the old notifications" + key: String! + type: NotificationType! + "The text for the title of the notification" + header: String! + "The text for the body of the notification" + content: String! + "A value between 0 and 1 when the notification type is `Progress`" + progress: Float + "Whether or not the message of the notification is positive, the UI might reflect this with a green color" + positive: Boolean! + "Whether or not the message of the notification is negative, the UI might reflect this with a red color" + negative: Boolean! + "Time in milliseconds before the notification should close" + timeout: Int +} + +type Subscription { + notification: Notification! +} + +"Specified the type a particular notification is of" +enum NotificationType { + "A regular message with no special additions" + Message + "A notification with an attached progress indicator" + Progress + "Close a notification with a given key" + Close +} diff --git a/api/graphql/resolvers/resolver.go b/api/graphql/resolvers/resolver.go new file mode 100644 index 0000000..bf01233 --- /dev/null +++ b/api/graphql/resolvers/resolver.go @@ -0,0 +1,24 @@ +package resolvers + +import ( + "context" + + "gorm.io/gorm" +) + +//go:generate go tool github.com/99designs/gqlgen + +type Resolver struct { + database *gorm.DB +} + +func NewRootResolver(db *gorm.DB) Resolver { + return Resolver{ + database: db, + } +} + +// DB returns a database instance that is tied to the given context +func (r *Resolver) DB(ctx context.Context) *gorm.DB { + return r.database.WithContext(ctx) +} diff --git a/api/graphql/resolvers/root.go b/api/graphql/resolvers/root.go new file mode 100644 index 0000000..a5db63f --- /dev/null +++ b/api/graphql/resolvers/root.go @@ -0,0 +1,19 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + api "github.com/photoview/photoview/api/graphql" +) + +// Mutation returns api.MutationResolver implementation. +func (r *Resolver) Mutation() api.MutationResolver { return &mutationResolver{r} } + +// Query returns api.QueryResolver implementation. +func (r *Resolver) Query() api.QueryResolver { return &queryResolver{r} } + +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/root.graphql b/api/graphql/resolvers/root.graphql new file mode 100644 index 0000000..e0b3f16 --- /dev/null +++ b/api/graphql/resolvers/root.graphql @@ -0,0 +1,47 @@ +directive @isAuthorized on FIELD_DEFINITION +directive @isAdmin on FIELD_DEFINITION + +scalar Time +scalar Any + +"Used to specify which order to sort items in" +enum OrderDirection { + "Sort accending A-Z" + ASC + "Sort decending Z-A" + DESC +} + +"Used to specify pagination on a list of items" +input Pagination { + "How many items to maximally fetch" + limit: Int + "How many items to skip from the beginning of the query, specified by the `Ordering`" + offset: Int +} + +"Used to specify how to sort items" +input Ordering { + "A column in the database to order by" + order_by: String + order_direction: OrderDirection +} + +type Query + +type Mutation + +""" +A group of media from the same album and the same day, that is grouped together in a timeline view +NOTE: It isn't used. Just copy from the old schema.graphql. +""" +type TimelineGroup { + "The full album containing the media in this timeline group" + album: Album! + "The media contained in this timeline group" + media: [Media!]! + "The total amount of media in this timeline group" + mediaTotal: Int! + "The day shared for all media in this timeline group" + date: Time! +} diff --git a/api/graphql/resolvers/scanner.go b/api/graphql/resolvers/scanner.go new file mode 100644 index 0000000..74252d3 --- /dev/null +++ b/api/graphql/resolvers/scanner.go @@ -0,0 +1,108 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/photoview/photoview/api/database/drivers" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/periodic_scanner" + "github.com/photoview/photoview/api/scanner/scanner_queue" + "gorm.io/gorm" +) + +// ScanAll is the resolver for the scanAll field. +func (r *mutationResolver) ScanAll(ctx context.Context) (*models.ScannerResult, error) { + err := scanner_queue.AddAllToQueue() + if err != nil { + return nil, err + } + + startMessage := "Scanner started" + + return &models.ScannerResult{ + Finished: false, + Success: true, + Message: &startMessage, + }, nil +} + +// ScanUser is the resolver for the scanUser field. +func (r *mutationResolver) ScanUser(ctx context.Context, userID int) (*models.ScannerResult, error) { + var user models.User + if err := r.DB(ctx).First(&user, userID).Error; err != nil { + return nil, fmt.Errorf("get user from database: %w", err) + } + + scanner_queue.AddUserToQueue(&user) + + startMessage := "Scanner started" + return &models.ScannerResult{ + Finished: false, + Success: true, + Message: &startMessage, + }, nil +} + +// SetPeriodicScanInterval is the resolver for the setPeriodicScanInterval field. +func (r *mutationResolver) SetPeriodicScanInterval(ctx context.Context, interval int) (int, error) { + db := r.DB(ctx) + if interval < 0 { + return 0, errors.New("interval must be 0 or above") + } + + if err := db. + Session(&gorm.Session{AllowGlobalUpdate: true}). + Model(&models.SiteInfo{}). + Update("periodic_scan_interval", interval). + Error; err != nil { + + return 0, err + } + + var siteInfo models.SiteInfo + if err := db.First(&siteInfo).Error; err != nil { + return 0, err + } + + periodic_scanner.ChangePeriodicScanInterval(time.Duration(siteInfo.PeriodicScanInterval) * time.Second) + + return siteInfo.PeriodicScanInterval, nil +} + +// SetScannerConcurrentWorkers is the resolver for the setScannerConcurrentWorkers field. +func (r *mutationResolver) SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error) { + db := r.DB(ctx) + if workers < 1 { + return 0, errors.New("concurrent workers must at least be 1") + } + + if workers > 1 && drivers.DatabaseDriverFromEnv() == drivers.SQLITE { + return 0, errors.New("multiple workers not supported for SQLite databases") + } + + if err := db. + Session(&gorm.Session{AllowGlobalUpdate: true}). + Model(&models.SiteInfo{}). + Update("concurrent_workers", workers). + Error; err != nil { + + return 0, err + } + + var siteInfo models.SiteInfo + if err := db.First(&siteInfo).Error; err != nil { + return 0, err + } + + scanner_queue.ChangeScannerConcurrentWorkers(siteInfo.ConcurrentWorkers) + + return siteInfo.ConcurrentWorkers, nil +} diff --git a/api/graphql/resolvers/scanner.graphql b/api/graphql/resolvers/scanner.graphql new file mode 100644 index 0000000..adc8fc7 --- /dev/null +++ b/api/graphql/resolvers/scanner.graphql @@ -0,0 +1,23 @@ +type ScannerResult { + finished: Boolean! + success: Boolean! + progress: Float + message: String +} + +extend type Mutation { + "Scan all users for new media" + scanAll: ScannerResult! @isAdmin + + "Scan a single user for new media" + scanUser(userId: ID!): ScannerResult! @isAdmin + + """ + Set how often, in seconds, the server should automatically scan for new media, + a value of 0 will disable periodic scans + """ + setPeriodicScanInterval(interval: Int!): Int! @isAdmin + + "Set max number of concurrent scanner jobs running at once" + setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin +} diff --git a/api/graphql/resolvers/search.go b/api/graphql/resolvers/search.go new file mode 100644 index 0000000..3eae3dc --- /dev/null +++ b/api/graphql/resolvers/search.go @@ -0,0 +1,24 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" +) + +// Search is the resolver for the search field. +func (r *queryResolver) Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.Search(r.DB(ctx), query, user.ID, limitMedia, limitAlbums) +} diff --git a/api/graphql/resolvers/search.graphql b/api/graphql/resolvers/search.graphql new file mode 100644 index 0000000..e6e2fb1 --- /dev/null +++ b/api/graphql/resolvers/search.graphql @@ -0,0 +1,13 @@ +type SearchResult { + "The string that was searched for" + query: String! + "A list of albums that matched the query" + albums: [Album!]! + "A list of media that matched the query" + media: [Media!]! +} + +extend type Query { + "Perform a search query on the contents of the media library" + search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult! +} diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go new file mode 100644 index 0000000..adf62da --- /dev/null +++ b/api/graphql/resolvers/share_token.go @@ -0,0 +1,167 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "errors" + "fmt" + "time" + + api "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// ShareAlbum is the resolver for the shareAlbum field. +func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.AddAlbumShare(r.DB(ctx), user, albumID, expire, password) +} + +// ShareMedia is the resolver for the shareMedia field. +func (r *mutationResolver) ShareMedia(ctx context.Context, mediaID int, expire *time.Time, password *string) (*models.ShareToken, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.AddMediaShare(r.DB(ctx), user, mediaID, expire, password) +} + +// DeleteShareToken is the resolver for the deleteShareToken field. +func (r *mutationResolver) DeleteShareToken(ctx context.Context, token string) (*models.ShareToken, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.DeleteShareToken(r.DB(ctx), user.ID, token) +} + +// ProtectShareToken is the resolver for the protectShareToken field. +func (r *mutationResolver) ProtectShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.ProtectShareToken(r.DB(ctx), user.ID, token, password) +} + +// SetExpireShareToken is the resolver for the setExpireShareToken field. +func (r *mutationResolver) SetExpireShareToken(ctx context.Context, token string, expire *time.Time) (*models.ShareToken, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.SetExpireShareToken(r.DB(ctx), user.ID, token, expire) +} + +// ShareToken is the resolver for the shareToken field. +func (r *queryResolver) ShareToken(ctx context.Context, credentials models.ShareTokenCredentials) (*models.ShareToken, error) { + var token models.ShareToken + if err := r.DB(ctx).Preload(clause.Associations).Where("value = ?", credentials.Token).First(&token).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("share not found") + } else { + return nil, fmt.Errorf("failed to get share token from database: %w", err) + } + } + + now := time.Now() + fakeTime := time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour(), + now.Minute(), + now.Second(), + 0, + time.UTC, + ) + + if token.Expire != nil && fakeTime.After(*token.Expire) { + return nil, errors.New("share expired") + } + + if token.Password != nil { + if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*credentials.Password)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + return nil, errors.New("unauthorized") + } else { + return nil, fmt.Errorf("failed to compare token password hashes: %w", err) + } + } + } + + return &token, nil +} + +// ShareTokenValidatePassword is the resolver for the shareTokenValidatePassword field. +func (r *queryResolver) ShareTokenValidatePassword(ctx context.Context, credentials models.ShareTokenCredentials) (bool, error) { + var token models.ShareToken + if err := r.DB(ctx).Where("value = ?", credentials.Token).First(&token).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, errors.New("share not found") + } else { + return false, fmt.Errorf("failed to get share token from database: %w", err) + } + } + now := time.Now() + fakeTime := time.Date( + now.Year(), + now.Month(), + now.Day(), + now.Hour(), + now.Minute(), + now.Second(), + 0, + time.UTC, + ) + //Assume the client’s local time is UTC, without applying any timezone conversion. + if token.Expire != nil && fakeTime.After(*token.Expire) { + return false, errors.New("share expired") + } + + if token.Password == nil { + return true, nil + } + + if credentials.Password == nil { + return false, nil + } + + if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*credentials.Password)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + return false, nil + } else { + return false, fmt.Errorf("could not compare token password hashes: %w", err) + } + } + + return true, nil +} + +// HasPassword is the resolver for the hasPassword field. +func (r *shareTokenResolver) HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error) { + hasPassword := obj.Password != nil + return hasPassword, nil +} + +// ShareToken returns api.ShareTokenResolver implementation. +func (r *Resolver) ShareToken() api.ShareTokenResolver { return &shareTokenResolver{r} } + +type shareTokenResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/share_token.graphql b/api/graphql/resolvers/share_token.graphql new file mode 100644 index 0000000..9f24f0f --- /dev/null +++ b/api/graphql/resolvers/share_token.graphql @@ -0,0 +1,47 @@ +"Credentials used to identify and authenticate a share token" +input ShareTokenCredentials { + token: String! + password: String +} + +"A token used to publicly access an album or media" +type ShareToken { + id: ID! + token: String! + "The user who created the token" + owner: User! + "Optional expire date" + expire: Time + "Whether or not a password is needed to access the share" + hasPassword: Boolean! + + "The album this token shares" + album: Album + "The media this token shares" + media: Media +} + +extend type Query { + "Fetch a share token containing an `Album` or `Media`" + shareToken(credentials: ShareTokenCredentials!): ShareToken! + + "Check if the `ShareToken` credentials are valid" + shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean! +} + +extend type Mutation { + "Generate share token for album" + shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized + + "Generate share token for media" + shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized + + "Delete a share token by it's token value" + deleteShareToken(token: String!): ShareToken! @isAuthorized + + "Set a password for a token, if null is passed for the password argument, the password will be cleared" + protectShareToken(token: String!, password: String): ShareToken! @isAuthorized + + "Set a Expiration Time for a token" + setExpireShareToken(token: String!,expire: Time):ShareToken! @isAuthorized +} diff --git a/api/graphql/resolvers/share_token_test.go b/api/graphql/resolvers/share_token_test.go new file mode 100644 index 0000000..87d07e6 --- /dev/null +++ b/api/graphql/resolvers/share_token_test.go @@ -0,0 +1,123 @@ +package resolvers + +import ( + "context" + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/bcrypt" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} + +func TestShareTokenValidatePassword(t *testing.T) { + test_utils.FilesystemTest(t) + db := test_utils.DatabaseTest(t) + pass := "1234" + user, err := models.RegisterUser(db, "test_user", &pass, true) + if err != nil { + t.Fatal("register user error:", err) + } + hashBytes, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + hashedPassword := string(hashBytes) + + now := time.Now() + expiredTime := now.Add(-24 * time.Hour) + expiredTime = time.Date( + expiredTime.Year(), + expiredTime.Month(), + expiredTime.Day(), + expiredTime.Hour(), + expiredTime.Minute(), + expiredTime.Second(), + 0, + time.UTC, + ) + futureTime := now.Add(24 * time.Hour) + futureTime = time.Date( + futureTime.Year(), + futureTime.Month(), + futureTime.Day(), + futureTime.Hour(), + futureTime.Minute(), + futureTime.Second(), + 0, + time.UTC, + ) + + db.AutoMigrate(&models.ShareToken{}) + testDataList := []models.ShareToken{ + { + Value: "EXPIRED_TOKEN", + OwnerID: user.ID, + Expire: &expiredTime, + }, + { + Value: "CORRECT_PASS", + OwnerID: user.ID, + Expire: &futureTime, + Password: &hashedPassword, + }, + } + db.Create(testDataList) + tests := []struct { + name string + credentials models.ShareTokenCredentials + wantResult bool + wantErr bool + wantErrMsg string + }{ + { + name: "Case 1: Token not exist", + credentials: models.ShareTokenCredentials{ + Token: "NOT_EXIST", + }, + wantResult: false, + wantErr: true, + wantErrMsg: "share not found", + }, + { + name: "Case 2: Token expired", + credentials: models.ShareTokenCredentials{ + Token: "EXPIRED_TOKEN", + }, + wantResult: false, + wantErr: true, + wantErrMsg: "share expired", + }, + { + name: "Case 3: correct pass", + credentials: models.ShareTokenCredentials{ + Token: "CORRECT_PASS", + Password: &pass, + }, + wantResult: true, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &queryResolver{ + Resolver: &Resolver{ + database: db, + }, + } + got, err := r.ShareTokenValidatePassword(context.Background(), tt.credentials) + if tt.wantErr { + assert.Error(t, err) + if tt.wantErrMsg != "" { + assert.Contains(t, err.Error(), tt.wantErrMsg) + } + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.wantResult, got) + }) + } +} diff --git a/api/graphql/resolvers/site_info.go b/api/graphql/resolvers/site_info.go new file mode 100644 index 0000000..4c9491b --- /dev/null +++ b/api/graphql/resolvers/site_info.go @@ -0,0 +1,29 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + + api "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" +) + +// SiteInfo is the resolver for the siteInfo field. +func (r *queryResolver) SiteInfo(ctx context.Context) (*models.SiteInfo, error) { + return models.GetSiteInfo(r.DB(ctx)) +} + +// FaceDetectionEnabled is the resolver for the faceDetectionEnabled field. +func (r *siteInfoResolver) FaceDetectionEnabled(ctx context.Context, obj *models.SiteInfo) (bool, error) { + return face_detection.GlobalFaceDetector != nil, nil +} + +// SiteInfo returns api.SiteInfoResolver implementation. +func (r *Resolver) SiteInfo() api.SiteInfoResolver { return &siteInfoResolver{r} } + +type siteInfoResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/site_info.graphql b/api/graphql/resolvers/site_info.graphql new file mode 100644 index 0000000..cc52cc0 --- /dev/null +++ b/api/graphql/resolvers/site_info.graphql @@ -0,0 +1,15 @@ +"General information about the site" +type SiteInfo { + "Whether or not the initial setup wizard should be shown" + initialSetup: Boolean! + "Whether or not face detection is enabled and working" + faceDetectionEnabled: Boolean! + "How often automatic scans should be initiated in seconds" + periodicScanInterval: Int! @isAdmin + "How many max concurrent scanner jobs that should run at once" + concurrentWorkers: Int! @isAdmin +} + +extend type Query { + siteInfo: SiteInfo! +} diff --git a/api/graphql/resolvers/timeline.go b/api/graphql/resolvers/timeline.go new file mode 100644 index 0000000..c9db9f0 --- /dev/null +++ b/api/graphql/resolvers/timeline.go @@ -0,0 +1,25 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "time" + + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" +) + +// MyTimeline is the resolver for the myTimeline field. +func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return actions.MyTimeline(r.DB(ctx), user, paginate, onlyFavorites, fromDate) +} diff --git a/api/graphql/resolvers/timeline.graphql b/api/graphql/resolvers/timeline.graphql new file mode 100644 index 0000000..6e3f3f9 --- /dev/null +++ b/api/graphql/resolvers/timeline.graphql @@ -0,0 +1,11 @@ +extend type Query { + """ + Get a list of media, ordered first by day, then by album if multiple media was found for the same day. + """ + myTimeline( + paginate: Pagination, + onlyFavorites: Boolean, + "Only fetch media that is older than this date" + fromDate: Time + ): [Media!]! @isAuthorized +} diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go new file mode 100644 index 0000000..84fc68b --- /dev/null +++ b/api/graphql/resolvers/user.go @@ -0,0 +1,343 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.90 + +import ( + "context" + "errors" + "fmt" + "path" + + api "github.com/photoview/photoview/api/graphql" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/scanner" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// AuthorizeUser is the resolver for the authorizeUser field. +func (r *mutationResolver) AuthorizeUser(ctx context.Context, username string, password string) (*models.AuthorizeResult, error) { + db := r.DB(ctx) + user, err := models.AuthorizeUser(db, username, password) + if err != nil { + return &models.AuthorizeResult{ + Success: false, + Status: err.Error(), + }, nil + } + + var token *models.AccessToken + + transactionError := db.Transaction(func(tx *gorm.DB) error { + token, err = user.GenerateAccessToken(tx) + if err != nil { + return err + } + + return nil + }) + + if transactionError != nil { + return nil, transactionError + } + + return &models.AuthorizeResult{ + Success: true, + Status: "ok", + Token: &token.Value, + }, nil +} + +// InitialSetupWizard is the resolver for the initialSetupWizard field. +func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) { + db := r.DB(ctx) + siteInfo, err := models.GetSiteInfo(db) + if err != nil { + return nil, err + } + + if !siteInfo.InitialSetup { + return nil, errors.New("not initial setup") + } + + rootPath = path.Clean(rootPath) + + var token *models.AccessToken + + transactionError := db.Transaction(func(tx *gorm.DB) error { + if err := tx.Exec("UPDATE site_info SET initial_setup = false").Error; err != nil { + return err + } + + user, err := models.RegisterUser(tx, username, &password, true) + if err != nil { + return err + } + + _, err = scanner.NewRootAlbum(tx, rootPath, user) + if err != nil { + return err + } + + token, err = user.GenerateAccessToken(tx) + if err != nil { + return err + } + + return nil + }) + + if transactionError != nil { + return &models.AuthorizeResult{ + Success: false, + Status: err.Error(), + }, nil + } + + return &models.AuthorizeResult{ + Success: true, + Status: "ok", + Token: &token.Value, + }, nil +} + +// UpdateUser is the resolver for the updateUser field. +func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) { + db := r.DB(ctx) + + if username == nil && password == nil && admin == nil { + return nil, errors.New("no updates requested") + } + + var user models.User + if err := db.First(&user, id).Error; err != nil { + return nil, err + } + + if username != nil { + user.Username = *username + } + + if password != nil { + hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12) + if err != nil { + return nil, err + } + hashedPass := string(hashedPassBytes) + + user.Password = &hashedPass + } + + if admin != nil { + user.Admin = *admin + } + + if err := db.Save(&user).Error; err != nil { + return nil, fmt.Errorf("failed to update user: %w", err) + } + + return &user, nil +} + +// CreateUser is the resolver for the createUser field. +func (r *mutationResolver) CreateUser(ctx context.Context, username string, password *string, admin bool) (*models.User, error) { + var user *models.User + + transactionError := r.DB(ctx).Transaction(func(tx *gorm.DB) error { + var err error + user, err = models.RegisterUser(tx, username, password, admin) + if err != nil { + return err + } + + return nil + }) + + if transactionError != nil { + return nil, transactionError + } + + return user, nil +} + +// DeleteUser is the resolver for the deleteUser field. +func (r *mutationResolver) DeleteUser(ctx context.Context, id int) (*models.User, error) { + return actions.DeleteUser(r.DB(ctx), id) +} + +// UserAddRootPath is the resolver for the userAddRootPath field. +func (r *mutationResolver) UserAddRootPath(ctx context.Context, id int, rootPath string) (*models.Album, error) { + db := r.DB(ctx) + + rootPath = path.Clean(rootPath) + + var user models.User + if err := db.First(&user, id).Error; err != nil { + return nil, err + } + + newAlbum, err := scanner.NewRootAlbum(db, rootPath, &user) + if err != nil { + return nil, err + } + + return newAlbum, nil +} + +// UserRemoveRootAlbum is the resolver for the userRemoveRootAlbum field. +func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error) { + db := r.DB(ctx) + + var album models.Album + if err := db.First(&album, albumID).Error; err != nil { + return nil, err + } + + var deletedAlbumIDs []int = nil + transactionError := db.Transaction(func(tx *gorm.DB) error { + if err := tx.Raw("DELETE FROM user_albums WHERE user_id = ? AND album_id = ?", userID, albumID).Error; err != nil { + return err + } + + children, err := album.GetChildren(tx, nil) + if err != nil { + return err + } + + childAlbumIDs := make([]int, len(children)) + for i, child := range children { + childAlbumIDs[i] = child.ID + } + + result := tx.Exec("DELETE FROM user_albums WHERE user_id = ? and album_id IN (?)", userID, childAlbumIDs) + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return errors.New("No relation deleted") + } + + // Cleanup if no user owns the album anymore + deletedAlbumIDs, err = cleanup(tx, albumID, childAlbumIDs) + if err != nil { + return err + } + + return nil + }) + + if transactionError != nil { + return nil, transactionError + } + + if err := clearCacheAndReloadFaces(db, deletedAlbumIDs); err != nil { + return nil, err + } + + return &album, nil +} + +// ChangeUserPreferences is the resolver for the changeUserPreferences field. +func (r *mutationResolver) ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) { + db := r.DB(ctx) + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + var langTrans *models.LanguageTranslation = nil + if language != nil { + lng := models.LanguageTranslation(*language) + langTrans = &lng + } + + var userPref models.UserPreferences + if err := db.Where("user_id = ?", user.ID).FirstOrInit(&userPref).Error; err != nil { + return nil, err + } + + userPref.UserID = user.ID + userPref.Language = langTrans + + if err := db.Save(&userPref).Error; err != nil { + return nil, err + } + + return &userPref, nil +} + +// User is the resolver for the user field. +func (r *queryResolver) User(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.User, error) { + var users []*models.User + + if err := models.FormatSQL(r.DB(ctx).Model(models.User{}), order, paginate).Find(&users).Error; err != nil { + return nil, err + } + + return users, nil +} + +// MyUser is the resolver for the myUser field. +func (r *queryResolver) MyUser(ctx context.Context) (*models.User, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return user, nil +} + +// MyUserPreferences is the resolver for the myUserPreferences field. +func (r *queryResolver) MyUserPreferences(ctx context.Context) (*models.UserPreferences, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + userPref := models.UserPreferences{ + UserID: user.ID, + } + if err := r.DB(ctx).Where("user_id = ?", user.ID).FirstOrCreate(&userPref).Error; err != nil { + return nil, err + } + + return &userPref, nil +} + +// Albums is the resolver for the albums field. +func (r *userResolver) Albums(ctx context.Context, obj *models.User) ([]*models.Album, error) { + obj.FillAlbums(r.DB(ctx)) + + pointerAlbums := make([]*models.Album, len(obj.Albums)) + for i, album := range obj.Albums { + pointerAlbums[i] = &album + } + + return pointerAlbums, nil +} + +// RootAlbums is the resolver for the rootAlbums field. +func (r *userResolver) RootAlbums(ctx context.Context, obj *models.User) (albums []*models.Album, err error) { + db := r.DB(ctx) + + err = db.Model(obj). + Where("albums.parent_album_id NOT IN (?)", + db.Table("user_albums"). + Select("albums.id"). + Joins("JOIN albums ON albums.id = user_albums.album_id AND user_albums.user_id = ?", obj.ID), + ).Or("albums.parent_album_id IS NULL").Order("path ASC"). + Association("Albums").Find(&albums) + + return +} + +// User returns api.UserResolver implementation. +func (r *Resolver) User() api.UserResolver { return &userResolver{r} } + +type userResolver struct{ *Resolver } diff --git a/api/graphql/resolvers/user.graphql b/api/graphql/resolvers/user.graphql new file mode 100644 index 0000000..f91cb3a --- /dev/null +++ b/api/graphql/resolvers/user.graphql @@ -0,0 +1,100 @@ +type User { + id: ID! + username: String! + "All albums owned by this user" + albums: [Album!]! @isAdmin + "Top level albums owned by this user" + rootAlbums: [Album!]! @isAdmin + "Whether or not the user has admin privileges" + admin: Boolean! +} + +"Supported language translations of the user interface" +enum LanguageTranslation { + English, + French, + Italian, + Swedish, + Danish, + Spanish, + Polish, + Ukrainian, + German, + Russian, + TraditionalChineseTW, + TraditionalChineseHK, + SimplifiedChinese, + Portuguese, + Basque, + Turkish, + Japanese, + Dutch, +} + +"Preferences for regular users" +type UserPreferences { + id: ID! + language: LanguageTranslation +} + +type AuthorizeResult { + success: Boolean! + + "A textual status message describing the result, can be used to show an error message when `success` is false" + status: String! + "An access token used to authenticate new API requests as the newly authorized user. Is present when success is true" + token: String +} + + +extend type Query { + "List of registered users, must be admin to call" + user(order: Ordering, paginate: Pagination): [User!]! @isAdmin + + "Information about the currently logged in user" + myUser: User! @isAuthorized + + "User preferences for the logged in user" + myUserPreferences: UserPreferences! @isAuthorized +} + +extend type Mutation { + "Authorizes a user and returns a token used to identify the new session" + authorizeUser(username: String!, password: String!): AuthorizeResult! + + "Registers the initial user, can only be called if initialSetup from SiteInfo is true" + initialSetupWizard( + username: String! + password: String! + rootPath: String! + ): AuthorizeResult + + "Update a user, fields left as `null` will not be changed" + updateUser( + id: ID! + username: String + password: String + admin: Boolean + ): User! @isAdmin + "Create a new user" + createUser( + username: String! + password: String + admin: Boolean! + ): User! @isAdmin + "Delete an existing user" + deleteUser(id: ID!): User! @isAdmin + + "Add a root path from where to look for media for the given user, specified by their user id." + userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin + + """ + Remove a root path from a user, specified by the id of the user and the top album representing the root path. + This album was returned when creating the path using `userAddRootPath`. + A list of root paths for a particular user can be retrived from the `User.rootAlbums` path. + """ + userRemoveRootAlbum(userId: ID!, albumId: ID!): Album @isAdmin + + "Change user preferences for the logged in user" + changeUserPreferences(language: String): UserPreferences! @isAuthorized +} diff --git a/api/graphql/resolvers/user.util.go b/api/graphql/resolvers/user.util.go new file mode 100644 index 0000000..c1290b5 --- /dev/null +++ b/api/graphql/resolvers/user.util.go @@ -0,0 +1,54 @@ +package resolvers + +import ( + "os" + "path" + "strconv" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" + "github.com/photoview/photoview/api/utils" + "gorm.io/gorm" +) + +func cleanup(tx *gorm.DB, albumID int, childAlbumIDs []int) ([]int, error) { + var userAlbumCount int + var deletedAlbumIDs []int = nil + + if err := tx.Raw("SELECT COUNT(user_id) FROM user_albums WHERE album_id = ?", + albumID).Scan(&userAlbumCount).Error; err != nil { + + return nil, err + } + + if userAlbumCount == 0 { + deletedAlbumIDs = append(childAlbumIDs, albumID) + childAlbumIDs = nil + // Delete albums from database + if err := tx.Delete(&models.Album{}, "id IN (?)", deletedAlbumIDs).Error; err != nil { + deletedAlbumIDs = nil + return nil, err + } + } + return deletedAlbumIDs, nil +} + +func clearCacheAndReloadFaces(db *gorm.DB, deletedAlbumIDs []int) error { + if deletedAlbumIDs != nil { + // Delete albums from cache + for _, id := range deletedAlbumIDs { + cacheAlbumPath := path.Join(utils.MediaCachePath(), strconv.Itoa(id)) + + if err := os.RemoveAll(cacheAlbumPath); err != nil { + return err + } + } + // Reload faces as media might have been deleted + if face_detection.GlobalFaceDetector != nil { + if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(db); err != nil { + return err + } + } + } + return nil +} diff --git a/api/log/context.go b/api/log/context.go new file mode 100644 index 0000000..a6c24ec --- /dev/null +++ b/api/log/context.go @@ -0,0 +1,41 @@ +package log + +import ( + "context" + "log/slog" +) + +var defaultLogger *slog.Logger + +func init() { + defaultLogger = slog.Default() +} + +type loggerKeyType string + +const loggerKey loggerKeyType = "logger" + +func getLogger(ctx context.Context) *slog.Logger { + if ctx == nil { + return defaultLogger + } + + logger := ctx.Value(loggerKey) + if logger == nil { + return defaultLogger + } + + ret, ok := logger.(*slog.Logger) + if !ok { + return defaultLogger + } + + return ret +} + +// WithAttrs creates a new context containing a new logger with [args] as logging attributes. +func WithAttrs(ctx context.Context, args ...any) context.Context { + old := getLogger(ctx) + new := old.With(args...) + return context.WithValue(ctx, loggerKey, new) +} diff --git a/api/log/context_test.go b/api/log/context_test.go new file mode 100644 index 0000000..825611d --- /dev/null +++ b/api/log/context_test.go @@ -0,0 +1,93 @@ +package log + +import ( + "bytes" + "context" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" + _ "github.com/photoview/photoview/api/test_utils/flags" +) + +func mockDefaultLogger(t *testing.T) *bytes.Buffer { + t.Helper() + + var output bytes.Buffer + handler := slog.NewJSONHandler(&output, &slog.HandlerOptions{ + AddSource: false, + Level: slog.LevelInfo, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Remove time from the output for predictable test output. + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + }) + + logger := slog.New(handler) + oldLogger := defaultLogger + defaultLogger = logger + + t.Cleanup(func() { + defaultLogger = oldLogger + }) + + return &output +} + +func TestLogger(t *testing.T) { + output := mockDefaultLogger(t) + + Debug(nil, "no_context") + Info(nil, "no_context") + Warn(nil, "no_context") + Error(nil, "no_context") + + bg := context.Background() + + Debug(bg, "bg_context") + Info(bg, "bg_context") + Warn(bg, "bg_context") + Error(bg, "bg_context") + + ctx1 := WithAttrs(context.Background(), "arg1", "value") + Debug(ctx1, "with_context_1") + Info(ctx1, "with_context_1") + Warn(ctx1, "with_context_1") + Error(ctx1, "with_context_1") + + ctx2 := WithAttrs(ctx1, "arg2", "value") + Debug(ctx2, "with_context_2") + Info(ctx2, "with_context_2") + Warn(ctx2, "with_context_2") + Error(ctx2, "with_context_2") + + Debug(ctx1, "with_context_1") + Info(ctx1, "with_context_1") + Warn(ctx1, "with_context_1") + Error(ctx1, "with_context_1") + + // Debug messages are filtered out due to slog.LevelInfo in mock logger + want := `{"level":"INFO","msg":"no_context"} +{"level":"WARN","msg":"no_context"} +{"level":"ERROR","msg":"no_context"} +{"level":"INFO","msg":"bg_context"} +{"level":"WARN","msg":"bg_context"} +{"level":"ERROR","msg":"bg_context"} +{"level":"INFO","msg":"with_context_1","arg1":"value"} +{"level":"WARN","msg":"with_context_1","arg1":"value"} +{"level":"ERROR","msg":"with_context_1","arg1":"value"} +{"level":"INFO","msg":"with_context_2","arg1":"value","arg2":"value"} +{"level":"WARN","msg":"with_context_2","arg1":"value","arg2":"value"} +{"level":"ERROR","msg":"with_context_2","arg1":"value","arg2":"value"} +{"level":"INFO","msg":"with_context_1","arg1":"value"} +{"level":"WARN","msg":"with_context_1","arg1":"value"} +{"level":"ERROR","msg":"with_context_1","arg1":"value"} +` + if diff := cmp.Diff(output.String(), want); diff != "" { + t.Errorf("diff: (-got, +want)\n%s", diff) + } + +} diff --git a/api/log/default.go b/api/log/default.go new file mode 100644 index 0000000..b19f9bf --- /dev/null +++ b/api/log/default.go @@ -0,0 +1,25 @@ +package log + +import ( + "context" +) + +// Debug logs debug messages. +func Debug(ctx context.Context, msg string, args ...any) { + getLogger(ctx).DebugContext(ctx, msg, args...) +} + +// Info logs info messages. +func Info(ctx context.Context, msg string, args ...any) { + getLogger(ctx).InfoContext(ctx, msg, args...) +} + +// Warn logs warning messages. +func Warn(ctx context.Context, msg string, args ...any) { + getLogger(ctx).WarnContext(ctx, msg, args...) +} + +// Error logs error messages. +func Error(ctx context.Context, msg string, args ...any) { + getLogger(ctx).ErrorContext(ctx, msg, args...) +} diff --git a/api/routes/authenticate_routes.go b/api/routes/authenticate_routes.go new file mode 100644 index 0000000..67a6f58 --- /dev/null +++ b/api/routes/authenticate_routes.go @@ -0,0 +1,155 @@ +package routes + +import ( + "fmt" + "net/http" + "time" + + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + + // "github.com/photoview/photoview/api/log" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +const internalServerError = "internal server error" + +func authenticateMedia(media *models.Media, db *gorm.DB, r *http.Request) (success bool, responseMessage string, responseStatus int, errorMessage error) { + user := auth.UserFromContext(r.Context()) + + if user != nil { + var album models.Album + if err := db.First(&album, media.AlbumID).Error; err != nil { + // log.Debug(nil, "Failed to find album for media %d: %v", media.ID, err) + return false, internalServerError, http.StatusInternalServerError, err + } + + ownsAlbum, err := user.OwnsAlbum(db, &album) + if err != nil { + // log.Debug(nil, "Failed to check if user owns album %d for media %d: %v", media.AlbumID, media.ID, err) + return false, internalServerError, http.StatusInternalServerError, err + } + + if !ownsAlbum { + // log.Debug(nil, "User does not own album %d for media %d", media.AlbumID, media.ID) + return false, "invalid credentials", http.StatusForbidden, nil + } + } else { + if success, respMsg, respStatus, err := shareTokenFromRequest(db, r, &media.ID, &media.AlbumID); !success { + return success, respMsg, respStatus, err + } + } + + return true, "success", http.StatusAccepted, nil +} + +func authenticateAlbum(album *models.Album, db *gorm.DB, r *http.Request) (success bool, responseMessage string, responseStatus int, errorMessage error) { + user := auth.UserFromContext(r.Context()) + + if user != nil { + ownsAlbum, err := user.OwnsAlbum(db, album) + if err != nil { + // log.Debug(nil, "Failed to check if user owns album %d: %v", album.ID, err) + return false, internalServerError, http.StatusInternalServerError, err + } + + if !ownsAlbum { + // log.Debug(nil, "User does not own album %d", album.ID) + return false, "invalid credentials", http.StatusForbidden, nil + } + } else { + if success, respMsg, respStatus, err := shareTokenFromRequest(db, r, nil, &album.ID); !success { + return success, respMsg, respStatus, err + } + } + + return true, "success", http.StatusAccepted, nil +} + +func shareTokenFromRequest(db *gorm.DB, r *http.Request, mediaID *int, albumID *int) (success bool, responseMessage string, responseStatus int, errorMessage error) { + // Check if photo is authorized with a share token + token := r.URL.Query().Get("token") + if token == "" { + return false, "unauthorized", http.StatusForbidden, errors.New("share token not provided") + } + + var shareToken models.ShareToken + + if err := db.Where("value = ?", token).First(&shareToken).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // log.Debug(nil, "Share token not found: %s", token) + return false, "unauthorized", http.StatusForbidden, errors.New("invalid share token") + } + // log.Debug(nil, "Error fetching share token: %s, error: %v", token, err) + return false, internalServerError, http.StatusInternalServerError, err + } + + if shareToken.Expire != nil && time.Now().UTC().After(shareToken.Expire.UTC()) { + // log.Debug(nil, "Share token expired: %s", token) + return false, "unauthorized", http.StatusForbidden, errors.New("invalid share token") + } + + // Validate share token password, if set + if shareToken.Password != nil { + tokenPasswordCookie, err := r.Cookie(fmt.Sprintf("share-token-pw-%s", shareToken.Value)) + if err != nil { + // log.Debug(nil, "Error getting share token password cookie: %v", err) + return false, "unauthorized", http.StatusForbidden, errors.Wrap(err, "share token password invalid") + } + // tokenPassword := r.Header.Get("TokenPassword") + tokenPassword := tokenPasswordCookie.Value + + if err := bcrypt.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + // log.Debug(nil, "Incorrect password for share token: %s", token) + return false, "unauthorized", http.StatusForbidden, errors.New("share token password invalid") + } else { + // log.Debug(nil, "Error comparing share token password: %s, error: %v", token, err) + return false, internalServerError, http.StatusInternalServerError, err + } + } + } + + if shareToken.AlbumID != nil && albumID == nil { + // log.Debug(nil, "Share token is of type album, but no albumID was provided to function") + return false, "unauthorized", http.StatusForbidden, errors.New("invalid share token") + } + + if shareToken.MediaID != nil && mediaID == nil { + // log.Debug(nil, "Share token is of type media, but no mediaID was provided to function") + return false, "unauthorized", http.StatusForbidden, errors.New("invalid share token") + } + + if shareToken.AlbumID != nil && *albumID != *shareToken.AlbumID { + // Check child albums + + var count int + err := db.Raw(` + WITH recursive child_albums AS ( + SELECT * FROM albums WHERE parent_album_id = ? + UNION ALL + SELECT child.* FROM albums child JOIN child_albums parent ON parent.id = child.parent_album_id + ) + SELECT COUNT(id) FROM child_albums WHERE id = ? + `, *shareToken.AlbumID, albumID).Find(&count).Error + + if err != nil { + // log.Debug(nil, "Error checking child albums for share token: %s, error: %v", token, err) + return false, internalServerError, http.StatusInternalServerError, err + } + + if count == 0 { + // log.Debug(nil, "No child albums found for share token: %s", token) + return false, "unauthorized", http.StatusForbidden, errors.New("invalid share token") + } + } + + if shareToken.MediaID != nil && *mediaID != *shareToken.MediaID { + // log.Debug(nil, "Media share token does not match mediaID: %d != %d", *mediaID, *shareToken.MediaID) + return false, "unauthorized", http.StatusForbidden, errors.New("invalid share token") + } + + return true, "", 0, nil +} diff --git a/api/routes/authenticate_routes_test.go b/api/routes/authenticate_routes_test.go new file mode 100644 index 0000000..e77effe --- /dev/null +++ b/api/routes/authenticate_routes_test.go @@ -0,0 +1,276 @@ +package routes + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestAuthenticateRoute(t *testing.T) { + const imageData = "IMAGE DATA" + const albumData = "ALBUM DATA" + + db := test_utils.DatabaseTest(t) + + user, err := models.RegisterUser(db, "username", nil, false) + if !assert.NoError(t, err) { + return + } + + album := models.Album{ + Title: "my_album", + Path: "/photos", + } + + if !assert.NoError(t, db.Model(&user).Association("Albums").Append(&album)) { + return + } + + media := models.Media{ + Title: "my_media", + Path: "/photos/image.jpg", + AlbumID: album.ID, + } + + if !assert.NoError(t, db.Save(&media).Error) { + return + } + + t.Run("Authenticate Media", func(t *testing.T) { + t.Run("Authorized request", func(t *testing.T) { + req := httptest.NewRequest("GET", "/photo/image.jpg", strings.NewReader(imageData)) + ctx := auth.AddUserToContext(req.Context(), user) + req = req.WithContext(ctx) + + success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req) + + assert.NoError(t, err) + assert.True(t, success) + assert.Equal(t, responseMessage, "success") + assert.Equal(t, responseStatus, http.StatusAccepted) + }) + + t.Run("Request without access token", func(t *testing.T) { + req := httptest.NewRequest("GET", "/photo/image.jpg", strings.NewReader(imageData)) + + success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req) + + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, responseMessage, "unauthorized") + assert.Equal(t, responseStatus, http.StatusForbidden) + }) + + expire := time.Now().Add(time.Hour * 24 * 30) + tokenPassword := "token-password-123" + shareToken, err := actions.AddMediaShare(db, user, media.ID, &expire, &tokenPassword) + if !assert.NoError(t, err) { + return + } + + t.Run("Request with share token", func(t *testing.T) { + url := fmt.Sprintf("/photo/image.jpg?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(imageData)) + + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value), + Value: tokenPassword, + } + req.AddCookie(&cookie) + + success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req) + + assert.NoError(t, err) + assert.True(t, success) + assert.Equal(t, "success", responseMessage) + assert.Equal(t, http.StatusAccepted, responseStatus) + }) + + t.Run("Request with invalid share token", func(t *testing.T) { + url := fmt.Sprintf("/photo/image.jpg?token=%s", "invalid-token") + req := httptest.NewRequest("GET", url, strings.NewReader(imageData)) + // Even if a cookie is sent, the token is invalid + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", "invalid-token"), + Value: "whatever", + } + req.AddCookie(&cookie) + success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + + t.Run("Request with share token but no password cookie", func(t *testing.T) { + shareToken, err := actions.AddMediaShare(db, user, media.ID, &expire, &tokenPassword) + assert.NoError(t, err) + url := fmt.Sprintf("/photo/image.jpg?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(imageData)) + // No cookie provided + success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + + t.Run("Request with share token and wrong password", func(t *testing.T) { + shareToken, err := actions.AddMediaShare(db, user, media.ID, &expire, &tokenPassword) + assert.NoError(t, err) + url := fmt.Sprintf("/photo/image.jpg?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(imageData)) + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value), + Value: "incorrect-password", + } + req.AddCookie(&cookie) + success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + + t.Run("Request with expired share token", func(t *testing.T) { + expired := time.Now().Add(-time.Hour) + shareToken, err := actions.AddMediaShare(db, user, media.ID, &expired, &tokenPassword) + assert.NoError(t, err) + url := fmt.Sprintf("/photo/image.jpg?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(imageData)) + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value), + Value: tokenPassword, + } + req.AddCookie(&cookie) + success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + }) + + t.Run("Authenticate Album", func(t *testing.T) { + t.Run("Authorized request", func(t *testing.T) { + req := httptest.NewRequest("GET", "/download/album/1", strings.NewReader(albumData)) + ctx := auth.AddUserToContext(req.Context(), user) + req = req.WithContext(ctx) + + success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req) + + assert.NoError(t, err) + assert.True(t, success) + assert.Equal(t, "success", responseMessage) + assert.Equal(t, http.StatusAccepted, responseStatus) + }) + + t.Run("Request without access token", func(t *testing.T) { + req := httptest.NewRequest("GET", "/download/album/1", strings.NewReader(albumData)) + + success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req) + + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + + expire := time.Now().Add(time.Hour * 24 * 30) + tokenPassword := "token-password-123" + shareToken, err := actions.AddAlbumShare(db, user, album.ID, &expire, &tokenPassword) + if !assert.NoError(t, err) { + return + } + + t.Run("Request with share token", func(t *testing.T) { + url := fmt.Sprintf("/download/album/1?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(albumData)) + + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value), + Value: tokenPassword, + } + req.AddCookie(&cookie) + + success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req) + + assert.NoError(t, err) + assert.True(t, success) + assert.Equal(t, "success", responseMessage) + assert.Equal(t, http.StatusAccepted, responseStatus) + }) + + t.Run("Request with invalid album share token", func(t *testing.T) { + url := fmt.Sprintf("/download/album/1?token=%s", "invalid-token") + req := httptest.NewRequest("GET", url, strings.NewReader(albumData)) + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", "invalid-token"), + Value: "whatever", + } + req.AddCookie(&cookie) + success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + + t.Run("Request with album share token but no password cookie", func(t *testing.T) { + shareToken, err := actions.AddAlbumShare(db, user, album.ID, &expire, &tokenPassword) + assert.NoError(t, err) + url := fmt.Sprintf("/download/album/1?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(albumData)) + // No cookie provided + success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + + t.Run("Request with album share token and wrong password", func(t *testing.T) { + shareToken, err := actions.AddAlbumShare(db, user, album.ID, &expire, &tokenPassword) + assert.NoError(t, err) + url := fmt.Sprintf("/download/album/1?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(albumData)) + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value), + Value: "incorrect-password", + } + req.AddCookie(&cookie) + success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + + t.Run("Request with expired album share token", func(t *testing.T) { + expired := time.Now().Add(-time.Hour) + shareToken, err := actions.AddAlbumShare(db, user, album.ID, &expired, &tokenPassword) + assert.NoError(t, err) + url := fmt.Sprintf("/download/album/1?token=%s", shareToken.Value) + req := httptest.NewRequest("GET", url, strings.NewReader(albumData)) + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value), + Value: tokenPassword, + } + req.AddCookie(&cookie) + success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req) + assert.Error(t, err) + assert.False(t, success) + assert.Equal(t, "unauthorized", responseMessage) + assert.Equal(t, http.StatusForbidden, responseStatus) + }) + }) +} diff --git a/api/routes/downloads.go b/api/routes/downloads.go new file mode 100644 index 0000000..de84765 --- /dev/null +++ b/api/routes/downloads.go @@ -0,0 +1,112 @@ +package routes + +import ( + "archive/zip" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + + "github.com/gorilla/mux" + "github.com/photoview/photoview/api/database/drivers" + "github.com/photoview/photoview/api/graphql/models" + "gorm.io/gorm" +) + +func RegisterDownloadRoutes(db *gorm.DB, router *mux.Router) { + router.HandleFunc("/album/{album_id}/{media_purpose}", func(w http.ResponseWriter, r *http.Request) { + albumID := mux.Vars(r)["album_id"] + mediaPurpose := mux.Vars(r)["media_purpose"] + mediaPurposeList := strings.SplitN(mediaPurpose, ",", 10) + + var album models.Album + if err := db.Find(&album, albumID).Error; err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404")) + return + } + + if success, response, status, err := authenticateAlbum(&album, db, r); !success { + if err != nil { + log.Printf("WARN: error authenticating album for download: %v\n", err) + } + w.WriteHeader(status) + w.Write([]byte(response)) + return + } + + var mediaWhereQuery string + if drivers.POSTGRES.MatchDatabase(db) { + mediaWhereQuery = "\"Media\".album_id = ?" + } else { + mediaWhereQuery = "Media.album_id = ?" + } + + var mediaURLs []*models.MediaURL + if err := db.Joins("Media").Where(mediaWhereQuery, album.ID).Where("media_urls.purpose IN (?)", mediaPurposeList).Find(&mediaURLs).Error; err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + if len(mediaURLs) == 0 { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("no media found")) + return + } + + // Do not allow caching + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", album.Title)) + + zipWriter := zip.NewWriter(w) + + for _, media := range mediaURLs { + zipFile, err := zipWriter.Create(fmt.Sprintf("%s/%s", album.Title, media.MediaName)) + if err != nil { + log.Printf("ERROR: Failed to create a file in zip, when downloading album (%d): %v\n", album.ID, err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + filePath, err := media.CachedPath() + if err != nil { + log.Printf("ERROR: Failed to get mediaURL cache path, when downloading album (%d): %v\n", album.ID, err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + fileData, err := os.Open(filePath) + if err != nil { + log.Printf("ERROR: Failed to open file to include in zip, when downloading album (%d): %v\n", album.ID, err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + _, err = io.Copy(zipFile, fileData) + if err != nil { + log.Printf("ERROR: Failed to copy file data, when downloading album (%d): %v\n", album.ID, err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + if err := fileData.Close(); err != nil { + log.Printf("ERROR: Failed to close file, when downloading album (%d): %v\n", album.ID, err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + } + + // close the zip Writer to flush the contents to the ResponseWriter + zipWriter.Close() + }) +} diff --git a/api/routes/photos.go b/api/routes/photos.go new file mode 100644 index 0000000..18d42b1 --- /dev/null +++ b/api/routes/photos.go @@ -0,0 +1,81 @@ +package routes + +import ( + "net/http" + "os" + + "github.com/gorilla/mux" + "gorm.io/gorm" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner" +) + +func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) { + + router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) { + mediaName := mux.Vars(r)["name"] + + var mediaURL models.MediaURL + result := db.Model(&models.MediaURL{}).Joins("Media").Select("media_urls.*").Where("media_urls.media_name = ?", mediaName).Scan(&mediaURL) + if err := result.Error; err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404")) + return + } + + media := mediaURL.Media + if media == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Media not found")) + return + } + + if success, response, status, err := authenticateMedia(media, db, r); !success { + if err != nil { + log.Warn(r.Context(), "Unauthorized access to photo", "reason", err) + } + w.WriteHeader(status) + w.Write([]byte(response)) + return + } + + cachedPath, err := mediaURL.CachedPath() + if err != nil { + log.Error(r.Context(), "error getting cached path for media URL", "error", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + if _, err := os.Stat(cachedPath); os.IsNotExist((err)) { + // err := db.Transaction(func(tx *gorm.DB) error { + if err = scanner.ProcessSingleMediaFunc(r.Context(), db, media); err != nil { + log.Error(r.Context(), "processing image not found in cache", + "media_cache_path", cachedPath, + "error", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + if _, err = os.Stat(cachedPath); err != nil { + log.Error(r.Context(), "after reprocessing image not found in cache", + "media_cache_path", cachedPath, + "error", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + } + + // Allow caching the resource + w.Header().Set("Cache-Control", "private, max-age=31536000, immutable") + if mediaURL.ContentType != "" { + w.Header().Set("Content-Type", mediaURL.ContentType) + } + + http.ServeFile(w, r, cachedPath) + }) +} diff --git a/api/routes/photos_test.go b/api/routes/photos_test.go new file mode 100644 index 0000000..4b3fd91 --- /dev/null +++ b/api/routes/photos_test.go @@ -0,0 +1,124 @@ +package routes + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner" + "github.com/photoview/photoview/api/test_utils" + "github.com/photoview/photoview/api/utils" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestPhotoRoutes(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user, err := models.RegisterUser(db, "testuser", nil, false) + assert.NoError(t, err) + album := models.Album{Title: "test_album", Path: "/photos"} + assert.NoError(t, db.Model(&user).Association("Albums").Append(&album)) + + media := models.Media{ + Title: "test_media", + Path: "/photos/test_image.jpg", + AlbumID: album.ID, + DateShot: time.Now(), + Type: models.MediaTypePhoto, + } + assert.NoError(t, db.Save(&media).Error) + + mediaURL := models.MediaURL{ + MediaID: media.ID, + Media: &media, + MediaName: "test_image.jpg", + Width: 1000, + Height: 800, + Purpose: models.PhotoThumbnail, + ContentType: "image/jpeg", + FileSize: 1024, + } + assert.NoError(t, db.Save(&mediaURL).Error) + + tempDir := t.TempDir() + orig := utils.MediaCachePath() + utils.ConfigureTestCache(tempDir) + defer utils.ConfigureTestCache(orig) + + router := mux.NewRouter() + RegisterPhotoRoutes(db, router) + + // -- Test cases -- + + // Non-existent media_name => 404 (no auth required) + t.Run("media not found", func(t *testing.T) { + req := httptest.NewRequest("GET", "/does_not_exist.jpg", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Equal(t, "404 - Media not found", rec.Body.String()) + }) + + // Missing auth => 403 + t.Run("auth failure", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test_image.jpg", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Contains(t, rec.Body.String(), "unauthorized") + }) + + // Cache miss + scan error => 500 + t.Run("scan failure yields 500", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test_image.jpg", nil) + ctx := auth.AddUserToContext(req.Context(), user) + req = req.WithContext(ctx) + + // ensure no cached file + cachedPath, err := mediaURL.CachedPath() + assert.NoError(t, err) + os.Remove(cachedPath) + + // mock scan to fail + origScan := scanner.ProcessSingleMediaFunc + scanner.ProcessSingleMediaFunc = func(ctx context.Context, db *gorm.DB, m *models.Media) error { + return fmt.Errorf("scan error") + } + defer func() { scanner.ProcessSingleMediaFunc = origScan }() + + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusInternalServerError, rec.Code) + assert.Contains(t, rec.Body.String(), "server error") + }) + + // Cache hit => 200 with correct body and headers + t.Run("cache hit serves file", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test_image.jpg", nil) + ctx := auth.AddUserToContext(req.Context(), user) + req = req.WithContext(ctx) + + // pre-create cached file + cachedPath, err := mediaURL.CachedPath() + assert.NoError(t, err) + assert.NoError(t, os.MkdirAll(path.Dir(cachedPath), 0755)) + content := []byte("cached-binary") + assert.NoError(t, os.WriteFile(cachedPath, content, 0644)) + + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "cached-binary", rec.Body.String()) + assert.Equal(t, "private, max-age=31536000, immutable", rec.Header().Get("Cache-Control")) + assert.Equal(t, "image/jpeg", rec.Header().Get("Content-Type")) + }) +} diff --git a/api/routes/routes_test.go b/api/routes/routes_test.go new file mode 100644 index 0000000..8eccb45 --- /dev/null +++ b/api/routes/routes_test.go @@ -0,0 +1,11 @@ +package routes_test + +import ( + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} diff --git a/api/routes/spa.go b/api/routes/spa.go new file mode 100644 index 0000000..c7cea39 --- /dev/null +++ b/api/routes/spa.go @@ -0,0 +1,274 @@ +package routes + +import ( + "fmt" + "mime" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/photoview/photoview/api/log" +) + +// SpaHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type SpaHandler struct { + staticPath string + indexPath string +} + +func NewSpaHandler(staticPath string, indexPath string) (SpaHandler, error) { + // Keeping this validation here just because we need the absolute static path anyway + staticPathAbs, err := filepath.Abs(staticPath) + if err != nil { + return SpaHandler{}, fmt.Errorf("static path %s is not valid: %w", staticPath, err) + } + + if err := validPath(staticPathAbs, true); err != nil { + return SpaHandler{}, fmt.Errorf("static path %s validation error: %w", staticPath, err) + } + if err := validPath(filepath.Join(staticPathAbs, indexPath), false); err != nil { + return SpaHandler{}, fmt.Errorf("index path %s validation error: %w", indexPath, err) + } + + return SpaHandler{ + indexPath: indexPath, + staticPath: staticPathAbs, + }, nil +} + +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +// Pre-compressed files (.br, .zst, .gz) are served if the client supports +// them, otherwise the original file is served. +func (h SpaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + relPath := strings.TrimPrefix(filepath.Clean(r.URL.Path), "/") + fullPath := filepath.Join(h.staticPath, relPath) + + r = r.WithContext(log.WithAttrs(r.Context(), "static_path", h.staticPath, "requested_path", r.URL.Path)) + + // Special case: root path should serve index.html + if relPath == "" { + h.serveIndexHTML(w, r) + return + } + + absPath, err := filepath.Abs(fullPath) + if err != nil { + log.Error(r.Context(), "error building absolute path", "error", err) + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + + rel, err := filepath.Rel(h.staticPath, absPath) + if err != nil || strings.Contains(rel, "..") { + log.Error(r.Context(), "requested path is outside of static path", "error", err) + http.Error(w, "Invalid request URI", http.StatusBadRequest) + return + } + + // Check if the original file exists + if _, err := os.Stat(absPath); os.IsNotExist(err) { + // File doesn't exist - let serveOriginal handle SPA routing + h.serveOriginal(w, r, fullPath, relPath) + return + } + + // Don't compress already-compressed files + if isCompressedFormat(filepath.Ext(absPath)) { + h.serveOriginal(w, r, fullPath, relPath) + return + } + // Try to serve pre-compressed file + if h.servePrecompressedFile(w, r, fullPath, relPath) { + return + } + + // Fallback to original SPA handler logic + h.serveOriginal(w, r, fullPath, relPath) +} + +func (h SpaHandler) serveOriginal(w http.ResponseWriter, r *http.Request, fullPath string, relPath string) { + // Check whether a file exists at the given path + _, err := os.Stat(fullPath) + if os.IsNotExist(err) { + // File does not exist, serve index.html (SPA routing) + h.serveIndexHTML(w, r) + return + } + if err != nil { + // If we got an error (that wasn't that the file doesn't exist) stating the file, + // return a 500 internal server error and stop + log.Error(r.Context(), "Error stating file, requested by client", "file_path", fullPath, "error", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Set cache headers + h.setCacheHeaders(w, relPath) + + // Use http.FileServer to serve the static file with no compression + http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) +} + +// servePrecompressedFile attempts to serve a pre-compressed variant of the file +// Returns true if a pre-compressed file was served, false otherwise +func (h SpaHandler) servePrecompressedFile(w http.ResponseWriter, r *http.Request, fullPath string, relPath string) bool { + // Parse Accept-Encoding header + acceptEncoding := filterZeroQuality(strings.ToLower(r.Header.Get("Accept-Encoding"))) + if acceptEncoding == "" { + return false + } + + // Check for pre-compressed variants in order of preference (br > zstd > gzip) + encodings := []struct { + name string + extension string + }{ + {"br", ".br"}, + {"zstd", ".zst"}, + {"gzip", ".gz"}, + } + + for _, enc := range encodings { + if !strings.Contains(acceptEncoding, enc.name) { + continue + } + + precompressedPath := fullPath + enc.extension + if stat, err := os.Stat(precompressedPath); err != nil || stat.IsDir() { + continue + } + + // Detect Content-Type from the ORIGINAL file extension, not the compressed one + contentType := mime.TypeByExtension(filepath.Ext(fullPath)) + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } + + w.Header().Set("Content-Encoding", enc.name) + w.Header().Add("Vary", "Accept-Encoding") + // Set cache headers based on request path + h.setCacheHeaders(w, relPath) + + // Serve pre-compressed file + http.ServeFile(w, r, precompressedPath) + return true + } + + return false +} + +// serveIndexHTML serves index.html with pre-compressed priority +func (h SpaHandler) serveIndexHTML(w http.ResponseWriter, r *http.Request) { + indexPath := filepath.Join(h.staticPath, h.indexPath) + + // Try to serve pre-compressed index.html first + // Use full and relative paths to index.html instead of originally requested paths + if h.servePrecompressedFile(w, r, indexPath, h.indexPath) { + return + } + + // Fallback to uncompressed index.html + if _, err := os.Stat(indexPath); os.IsNotExist(err) { + // Index file doesn't exist - this is a serious configuration error, not a regular 404 + log.Error(r.Context(), "Error: index.html not found", "index_path", indexPath) + http.Error(w, "Application index file not found", http.StatusInternalServerError) + return + } + + h.setCacheHeaders(w, h.indexPath) + http.ServeFile(w, r, indexPath) +} + +// setCacheHeaders sets appropriate cache headers based on the request path +func (h SpaHandler) setCacheHeaders(w http.ResponseWriter, relPath string) { + if strings.HasPrefix(relPath, "assets/") { + // Long-term cache for fingerprinted assets + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } else { + // Short cache with revalidation for other files + w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate") + } +} + +// validPath validates the given path string. +// +// Parameters: +// - pathStr: The path to validate. +// - isDir: A boolean indicating whether the path is expected to be a directory (true) or a file (false). +// +// Returns: +// - error: An error if the path is invalid, or nil if the path is valid. +func validPath(pathStr string, isDir bool) error { + pathAbs, err := filepath.Abs(pathStr) + if err != nil { + return fmt.Errorf("path %s is not valid: %w", pathStr, err) + } + + stat, err := os.Stat(pathAbs) + if os.IsNotExist(err) { + return fmt.Errorf("path %s does not exist", pathAbs) + } + if os.IsPermission(err) { + return fmt.Errorf("no permission to access path %s", pathAbs) + } + if err != nil { + return fmt.Errorf("error accessing path %s: %w", pathAbs, err) + } + if isDir && !stat.IsDir() { + return fmt.Errorf("path %s is not a directory", pathAbs) + } + if !isDir && stat.IsDir() { + return fmt.Errorf("path %s is a directory, must be a file", pathAbs) + } + return nil +} + +func isCompressedFormat(ext string) bool { + ext = strings.ToLower(ext) + compressedExts := []string{ + ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp", + ".zip", ".gz", ".br", ".zst", + ".woff", ".woff2", + } + + for _, compExt := range compressedExts { + if ext == compExt { + return true + } + } + return false +} + +// filterZeroQuality removes encodings with q=0 or q=0.0 from Accept-Encoding header +func filterZeroQuality(acceptEncoding string) string { + // If no q params, return unchanged (fast path for most requests) + if !strings.Contains(acceptEncoding, "q=") { + return acceptEncoding + } + + var filtered []string + parts := strings.Split(acceptEncoding, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + // Check if this encoding has q=0 or q=0.0 + normalized := strings.ReplaceAll(part, " ", "") + if strings.HasSuffix(normalized, ";q=0") || strings.HasSuffix(normalized, ";q=0.0") { + continue + } + + filtered = append(filtered, part) + } + + return strings.Join(filtered, ",") +} diff --git a/api/routes/spa_test.go b/api/routes/spa_test.go new file mode 100644 index 0000000..050c2e0 --- /dev/null +++ b/api/routes/spa_test.go @@ -0,0 +1,554 @@ +package routes + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewSpaHandler(t *testing.T) { + t.Run("valid paths", func(t *testing.T) { + tempDir := t.TempDir() + indexPath := filepath.Join(tempDir, "index.html") + require.NoError(t, os.WriteFile(indexPath, []byte("index content"), 0644)) + + handler, err := NewSpaHandler(tempDir, "index.html") + assert.NoError(t, err) + assert.NotEmpty(t, handler.staticPath) + assert.NotEmpty(t, handler.indexPath) + }) + + t.Run("invalid static path", func(t *testing.T) { + handler, err := NewSpaHandler("/nonexistent/path", "index.html") + assert.Error(t, err) + assert.Empty(t, handler.staticPath) + assert.Empty(t, handler.indexPath) + }) + + t.Run("static path is not a directory", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "file.txt") + require.NoError(t, os.WriteFile(filePath, []byte("content"), 0644)) + + handler, err := NewSpaHandler(filePath, "index.html") + assert.Error(t, err) + assert.Empty(t, handler.staticPath) + assert.Empty(t, handler.indexPath) + }) + + t.Run("index path does not exist", func(t *testing.T) { + tempDir := t.TempDir() + handler, err := NewSpaHandler(tempDir, "nonexistent.html") + assert.Error(t, err) + assert.Empty(t, handler.staticPath) + assert.Empty(t, handler.indexPath) + }) + + t.Run("index path is a directory", func(t *testing.T) { + tempDir := t.TempDir() + indexDir := filepath.Join(tempDir, "indexdir") + require.NoError(t, os.Mkdir(indexDir, 0755)) + + handler, err := NewSpaHandler(tempDir, "indexdir") + assert.Error(t, err) + assert.Empty(t, handler.staticPath) + assert.Empty(t, handler.indexPath) + }) +} + +func TestSpaHandler_ServeHTTP(t *testing.T) { + // Setup test directory structure + tempDir := t.TempDir() + + // Create index.html + indexPath := filepath.Join(tempDir, "index.html") + require.NoError(t, os.WriteFile(indexPath, []byte("index"), 0644)) + + // Create assets directory + assetsDir := filepath.Join(tempDir, "assets") + require.NoError(t, os.Mkdir(assetsDir, 0755)) + + // Create test files + jsFile := filepath.Join(assetsDir, "app.js") + require.NoError(t, os.WriteFile(jsFile, []byte("console.log('app');"), 0644)) + + // Create pre-compressed variants + require.NoError(t, os.WriteFile(jsFile+".br", []byte("compressed-br"), 0644)) + require.NoError(t, os.WriteFile(jsFile+".zst", []byte("compressed-zst"), 0644)) + require.NoError(t, os.WriteFile(jsFile+".gz", []byte("compressed-gz"), 0644)) + + // Create an image file (should not be compressed) + imgFile := filepath.Join(assetsDir, "logo.png") + require.NoError(t, os.WriteFile(imgFile, []byte("PNG data"), 0644)) + + handler, err := NewSpaHandler(tempDir, "index.html") + require.NoError(t, err) + require.NotEmpty(t, handler.staticPath) + + t.Run("serve root returns index.html", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "index") + }) + + t.Run("serve existing file", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "console.log") + }) + + t.Run("serve non-existent file returns index.html (SPA routing)", func(t *testing.T) { + req := httptest.NewRequest("GET", "/some/spa/route", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "index") + }) + + t.Run("path traversal attack prevented", func(t *testing.T) { + req := httptest.NewRequest("GET", "/../../../etc/passwd", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusBadRequest, rec.Code) + }) + + t.Run("in-tree traversal redirected", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/../index.html", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusMovedPermanently, rec.Code) + }) + + t.Run("empty handler configuration returns 500", func(t *testing.T) { + emptyHandler := SpaHandler{} + req := httptest.NewRequest("GET", "/", nil) + rec := httptest.NewRecorder() + + emptyHandler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusInternalServerError, rec.Code) + }) +} + +func TestSpaHandler_PrecompressedFiles(t *testing.T) { + tempDir := t.TempDir() + + // Create index.html + indexPath := filepath.Join(tempDir, "index.html") + require.NoError(t, os.WriteFile(indexPath, []byte("index"), 0644)) + + // Create assets directory + assetsDir := filepath.Join(tempDir, "assets") + require.NoError(t, os.Mkdir(assetsDir, 0755)) + + // Create test file with all compression variants + jsFile := filepath.Join(assetsDir, "app.js") + jsContent := []byte("console.log('original');") + require.NoError(t, os.WriteFile(jsFile, jsContent, 0644)) + require.NoError(t, os.WriteFile(jsFile+".br", []byte("br-compressed"), 0644)) + require.NoError(t, os.WriteFile(jsFile+".zst", []byte("zst-compressed"), 0644)) + require.NoError(t, os.WriteFile(jsFile+".gz", []byte("gz-compressed"), 0644)) + + handler, err := NewSpaHandler(tempDir, "index.html") + require.NoError(t, err) + + t.Run("serve brotli when accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + req.Header.Set("Accept-Encoding", "gzip, deflate, br") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "br", rec.Header().Get("Content-Encoding")) + assert.Contains(t, rec.Header().Get("Vary"), "Accept-Encoding") + assert.Contains(t, rec.Header().Get("Content-Type"), "javascript") + assert.Equal(t, "br-compressed", rec.Body.String()) + }) + + t.Run("serve zstd when br not accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + req.Header.Set("Accept-Encoding", "gzip, zstd") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "zstd", rec.Header().Get("Content-Encoding")) + assert.Contains(t, rec.Header().Get("Vary"), "Accept-Encoding") + assert.Contains(t, rec.Header().Get("Content-Type"), "javascript") + assert.Equal(t, "zst-compressed", rec.Body.String()) + }) + + t.Run("serve gzip when only gzip accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + req.Header.Set("Accept-Encoding", "gzip") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "gzip", rec.Header().Get("Content-Encoding")) + assert.Contains(t, rec.Header().Get("Vary"), "Accept-Encoding") + assert.Contains(t, rec.Header().Get("Content-Type"), "javascript") + assert.Equal(t, "gz-compressed", rec.Body.String()) + }) + + t.Run("serve uncompressed when no encoding accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Empty(t, rec.Header().Get("Content-Encoding")) + assert.Contains(t, rec.Body.String(), "original") + }) + + t.Run("serve original when pre-compressed file missing", func(t *testing.T) { + // Create file without pre-compressed variants + cssFile := filepath.Join(assetsDir, "style.css") + require.NoError(t, os.WriteFile(cssFile, []byte("body { margin: 0; }"), 0644)) + + req := httptest.NewRequest("GET", "/assets/style.css", nil) + req.Header.Set("Accept-Encoding", "gzip, br") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Empty(t, rec.Header().Get("Content-Encoding")) + assert.Contains(t, rec.Body.String(), "margin") + }) + + t.Run("do not compress already-compressed formats", func(t *testing.T) { + pngFile := filepath.Join(assetsDir, "logo.png") + require.NoError(t, os.WriteFile(pngFile, []byte("PNG data"), 0644)) + require.NoError(t, os.WriteFile(pngFile+".br", []byte("should-not-serve"), 0644)) + + req := httptest.NewRequest("GET", "/assets/logo.png", nil) + req.Header.Set("Accept-Encoding", "br") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Empty(t, rec.Header().Get("Content-Encoding")) + assert.Equal(t, "PNG data", rec.Body.String()) + }) +} + +func TestSpaHandler_CacheHeaders(t *testing.T) { + tempDir := t.TempDir() + + indexPath := filepath.Join(tempDir, "index.html") + require.NoError(t, os.WriteFile(indexPath, []byte("index"), 0644)) + + assetsDir := filepath.Join(tempDir, "assets") + require.NoError(t, os.Mkdir(assetsDir, 0755)) + + jsFile := filepath.Join(assetsDir, "app.js") + require.NoError(t, os.WriteFile(jsFile, []byte("console.log('app');"), 0644)) + + otherFile := filepath.Join(tempDir, "manifest.json") + require.NoError(t, os.WriteFile(otherFile, []byte("{}"), 0644)) + + handler, err := NewSpaHandler(tempDir, "index.html") + require.NoError(t, err) + + t.Run("long-term cache for assets", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "public, max-age=31536000, immutable", rec.Header().Get("Cache-Control")) + }) + + t.Run("short cache with revalidation for non-assets", func(t *testing.T) { + req := httptest.NewRequest("GET", "/manifest.json", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "public, max-age=3600, must-revalidate", rec.Header().Get("Cache-Control")) + }) + + t.Run("cache headers for index.html", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "public, max-age=3600, must-revalidate", rec.Header().Get("Cache-Control")) + }) +} + +func TestSpaHandler_IndexHTML(t *testing.T) { + tempDir := t.TempDir() + + indexPath := filepath.Join(tempDir, "index.html") + indexContent := []byte("index") + require.NoError(t, os.WriteFile(indexPath, indexContent, 0644)) + + // Create pre-compressed index.html + require.NoError(t, os.WriteFile(indexPath+".br", []byte("index-br-compressed"), 0644)) + + handler, err := NewSpaHandler(tempDir, "index.html") + require.NoError(t, err) + + t.Run("serve pre-compressed index.html when accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("Accept-Encoding", "br") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "br", rec.Header().Get("Content-Encoding")) + assert.Equal(t, "index-br-compressed", rec.Body.String()) + }) + + t.Run("serve uncompressed index.html when encoding not accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Empty(t, rec.Header().Get("Content-Encoding")) + assert.Contains(t, rec.Body.String(), "index") + }) + + t.Run("SPA routing serves index.html for non-existent paths", func(t *testing.T) { + req := httptest.NewRequest("GET", "/app/users/123", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "index") + }) +} + +func TestIsCompressedFormat(t *testing.T) { + tests := []struct { + name string + ext string + expected bool + }{ + {"png", ".png", true}, + {"jpg", ".jpg", true}, + {"jpeg", ".jpeg", true}, + {"gif", ".gif", true}, + {"webp", ".webp", true}, + {"ico", ".ico", true}, + {"zip", ".zip", true}, + {"gz", ".gz", true}, + {"br", ".br", true}, + {"zst", ".zst", true}, + {"woff", ".woff", true}, + {"woff2", ".woff2", true}, + {"js", ".js", false}, + {"css", ".css", false}, + {"html", ".html", false}, + {"json", ".json", false}, + {"txt", ".txt", false}, + {"uppercase PNG", ".PNG", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isCompressedFormat(tt.ext) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFilterZeroQuality(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no q params (fast path)", + input: "gzip, deflate, br", + expected: "gzip, deflate, br", + }, + { + name: "filter q=0", + input: "gzip;q=1.0, deflate;q=0, br;q=0.8", + expected: "gzip;q=1.0,br;q=0.8", + }, + { + name: "filter q=0.0", + input: "gzip;q=0.0, br;q=1", + expected: "br;q=1", + }, + { + name: "filter with spaces", + input: "gzip; q=0.0, deflate; q=1.0, br", + expected: "deflate; q=1.0,br", + }, + { + name: "all encodings filtered", + input: "gzip;q=0, deflate;q=0.0", + expected: "", + }, + { + name: "empty input", + input: "", + expected: "", + }, + { + name: "q=0 but others present", + input: "identity;q=0, gzip, br", + expected: "gzip,br", + }, + { + name: "multiple spaces", + input: "gzip; q=0, br; q=1", + expected: "br; q=1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filterZeroQuality(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestSpaHandler_AcceptEncodingEdgeCases(t *testing.T) { + tempDir := t.TempDir() + + indexPath := filepath.Join(tempDir, "index.html") + require.NoError(t, os.WriteFile(indexPath, []byte("index"), 0644)) + + assetsDir := filepath.Join(tempDir, "assets") + require.NoError(t, os.Mkdir(assetsDir, 0755)) + + jsFile := filepath.Join(assetsDir, "app.js") + require.NoError(t, os.WriteFile(jsFile, []byte("original"), 0644)) + require.NoError(t, os.WriteFile(jsFile+".br", []byte("br-compressed"), 0644)) + require.NoError(t, os.WriteFile(jsFile+".gz", []byte("gz-compressed"), 0644)) + + handler, err := NewSpaHandler(tempDir, "index.html") + require.NoError(t, err) + + t.Run("reject encoding with q=0", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + req.Header.Set("Accept-Encoding", "br;q=0, gzip") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + // Should serve gzip since br is rejected with q=0 + assert.Equal(t, "gzip", rec.Header().Get("Content-Encoding")) + }) + + t.Run("case insensitive encoding names", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + req.Header.Set("Accept-Encoding", "BR, GZIP") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "br", rec.Header().Get("Content-Encoding")) + }) + + t.Run("wildcard encoding", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + req.Header.Set("Accept-Encoding", "*") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + // Wildcard doesn't match our specific encodings + assert.Empty(t, rec.Header().Get("Content-Encoding")) + }) + + t.Run("complex Accept-Encoding with quality values", func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/app.js", nil) + req.Header.Set("Accept-Encoding", "gzip;q=0.8, br;q=1.0, deflate;q=0.5") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + // br has highest quality and should be preferred + assert.Equal(t, "br", rec.Header().Get("Content-Encoding")) + }) +} + +func TestSpaHandler_ContentType(t *testing.T) { + tempDir := t.TempDir() + + indexPath := filepath.Join(tempDir, "index.html") + require.NoError(t, os.WriteFile(indexPath, []byte("index"), 0644)) + + assetsDir := filepath.Join(tempDir, "assets") + require.NoError(t, os.Mkdir(assetsDir, 0755)) + + // Create files with various extensions + files := map[string]string{ + "app.js": "text/javascript", + "style.css": "text/css", + "data.json": "application/json", + "image.svg": "image/svg+xml", + "unknown.xyz": "", // Unknown extension + } + + for filename := range files { + filePath := filepath.Join(assetsDir, filename) + require.NoError(t, os.WriteFile(filePath, []byte("content"), 0644)) + require.NoError(t, os.WriteFile(filePath+".br", []byte("compressed"), 0644)) + } + + handler, err := NewSpaHandler(tempDir, "index.html") + require.NoError(t, err) + + for filename, expectedType := range files { + t.Run(filename, func(t *testing.T) { + req := httptest.NewRequest("GET", "/assets/"+filename, nil) + req.Header.Set("Accept-Encoding", "br") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "br", rec.Header().Get("Content-Encoding")) + + if expectedType != "" { + assert.Contains(t, rec.Header().Get("Content-Type"), expectedType) + } + }) + } +} diff --git a/api/routes/videos.go b/api/routes/videos.go new file mode 100644 index 0000000..27e45f8 --- /dev/null +++ b/api/routes/videos.go @@ -0,0 +1,137 @@ +package routes + +import ( + "context" + "net/http" + "os" + "path" + "strconv" + + "github.com/gorilla/mux" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +var processSingleMediaFn = func(ctx context.Context, db *gorm.DB, media *models.Media) error { + return scanner.ProcessSingleMedia(ctx, db, media) +} + +func handleVideoRequest( + w http.ResponseWriter, + r *http.Request, + db *gorm.DB, + mediaName string, + authenticateFn func(*models.Media, *gorm.DB, *http.Request) (bool, string, int, error), + getCachePathFn func(albumID, mediaID int, filename string) string, +) { + var mediaURLs []models.MediaURL + if err := db.Model(&models.MediaURL{}). + Preload("Media"). + Where("media_urls.media_name = ? AND media_urls.purpose = ?", mediaName, models.VideoWeb). + Order("created_at DESC"). + Find(&mediaURLs). + Error; err != nil || len(mediaURLs) == 0 || mediaURLs[0].Media == nil { + + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("not found")) + return + } + + if len(mediaURLs) > 1 { + log.Warn(r.Context(), "Multiple video web URLs found", + "name", mediaName, + "count", len(mediaURLs), + "using", mediaURLs[0], + ) + } + + mediaURL := mediaURLs[0] + var media = mediaURL.Media + + if success, response, status, err := authenticateFn(media, db, r); !success { + if err != nil { + log.Warn(r.Context(), "got error authenticating video", + "error", err, + "media ID", media.ID, + "media path", media.Path) + } + w.WriteHeader(status) + w.Write([]byte(response)) + return + } + + var cachedPath string + + if mediaURL.Purpose == models.VideoWeb { + // Use the provided cache path function + cachedPath = getCachePathFn(int(media.AlbumID), int(mediaURL.MediaID), mediaURL.MediaName) + } else { + log.Error(r.Context(), "Can not handle media_purpose for video", + "purpose", mediaURL.Purpose, + "expected", models.VideoWeb) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + if _, err := os.Stat(cachedPath); err != nil { + if !os.IsNotExist(err) { + log.Error(r.Context(), "cached video access error", + "error", err, + "media ID", media.ID, + "media path", media.Path) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + if err := processSingleMediaFn(r.Context(), db, media); err != nil { + // Check if error was due to context cancellation + if r.Context().Err() != nil && errors.Is(r.Context().Err(), context.Canceled) { + log.Warn(r.Context(), "video processing cancelled due to client disconnect", + "mediaID", media.ID, + "reason", r.Context().Err()) + return // Don't send response if client disconnected + } + + log.Error(r.Context(), "processing video not found in cache", + "error", err, + "media ID", media.ID, + "media path", media.Path) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + + if _, err := os.Stat(cachedPath); err != nil { + log.Error(r.Context(), "video not found in cache after reprocessing", + "error", err, + "media ID", media.ID, + "media path", media.Path) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(internalServerError)) + return + } + } + + // Allow caching the resource + w.Header().Set("Cache-Control", "private, max-age=31536000, immutable") + w.Header().Set("Content-Type", mediaURL.ContentType) + http.ServeFile(w, r, cachedPath) +} + +func generateCacheFilename(albumID, mediaID int, filename string) string { + return path.Join(utils.MediaCachePath(), strconv.Itoa(albumID), strconv.Itoa(mediaID), filename) +} + +func RegisterVideoRoutes(db *gorm.DB, router *mux.Router) { + + router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) { + mediaName := mux.Vars(r)["name"] + handleVideoRequest(w, r, db, mediaName, authenticateMedia, generateCacheFilename) + }) +} diff --git a/api/routes/videos_test.go b/api/routes/videos_test.go new file mode 100644 index 0000000..f89a6cb --- /dev/null +++ b/api/routes/videos_test.go @@ -0,0 +1,471 @@ +package routes + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/photoview/photoview/api/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +// setTestCachePath temporarily sets a different media cache path for testing +// and returns a function to restore the original state +func setTestCachePath(tempPath string) func() { + original := utils.GetTestCachePath() + utils.ConfigureTestCache(tempPath) + return func() { + utils.ConfigureTestCache(original) + } +} + +// mockProcessSingleMedia replaces scanner.ProcessSingleMedia with a mock function during tests +// and returns a function to restore the original implementation +var originalProcessSingleMedia = processSingleMediaFn + +func mockProcessSingleMedia(t *testing.T, shouldSucceed bool, mediaID int, albumID int) func() { + // Save original implementation + savedFn := processSingleMediaFn + + // Replace with mock implementation + processSingleMediaFn = func(ctx context.Context, db *gorm.DB, media *models.Media) error { + // Check if context is already cancelled before starting work + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + if shouldSucceed { + // On success: create the expected video file in cache + var mediaURLs []models.MediaURL + if err := db.Where("media_id = ? AND purpose = ?", media.ID, models.VideoWeb). + Find(&mediaURLs).Error; err != nil { + return err + } + + if len(mediaURLs) == 0 { + return fmt.Errorf("no media URLs found") + } + + // Get the cache path + tempCachePath := utils.GetTestCachePath() + albumDir := filepath.Join(tempCachePath, strconv.Itoa(albumID)) + mediaDir := filepath.Join(albumDir, strconv.Itoa(mediaID)) + if err := os.MkdirAll(mediaDir, 0755); err != nil { + return err + } + + videoPath := filepath.Join(mediaDir, mediaURLs[0].MediaName) + if err := os.WriteFile(videoPath, []byte("mocked processed video content"), 0644); err != nil { + return fmt.Errorf("failed to write mock video file: %w", err) + } + return nil + } + + // On failure: return an error + return fmt.Errorf("mock processing error") + } + + // Return cleanup function + return func() { + processSingleMediaFn = savedFn + } +} + +func registerMockVideoRoutesForTesting(db *gorm.DB, router *mux.Router, tempCachePath string) { + router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) { + mediaName := mux.Vars(r)["name"] + + // Use no-op auth and test cache path + handleVideoRequest( + w, r, db, mediaName, + // Skip authentication for tests + func(media *models.Media, db *gorm.DB, r *http.Request) (bool, string, int, error) { + return true, "success", http.StatusOK, nil + }, + // Use test cache path + func(albumID, mediaID int, filename string) string { + return path.Join(tempCachePath, strconv.Itoa(albumID), strconv.Itoa(mediaID), filename) + }, + ) + }) +} + +// createTestResources creates all the necessary test resources for a single test case +// and returns cleanup functions to be called with t.Cleanup() +func createTestResources(t *testing.T, db *gorm.DB, testID string) ( + *models.User, + *models.Album, + *models.Media, + *models.MediaURL, + string, // mediaName + string, // cachePath + string, // shareToken + string, // tokenPassword +) { + // Create test user with unique username + user := &models.User{ + Username: fmt.Sprintf("testuser-%s", testID), + } + require.NoError(t, db.Create(user).Error) + t.Cleanup(func() { + db.Unscoped().Delete(user) + }) + + // Create test album with unique title and path + album := &models.Album{ + Title: fmt.Sprintf("Test Album %s", testID), + Path: fmt.Sprintf("/test/album/path/%s", testID), + } + require.NoError(t, db.Create(album).Error) + t.Cleanup(func() { + db.Unscoped().Delete(album) + }) + + // Establish ownership via many-to-many relationship + require.NoError(t, db.Model(album).Association("Owners").Append(user)) + t.Cleanup(func() { + db.Model(album).Association("Owners").Clear() + }) + + // Create unique media name for this test + mediaName := fmt.Sprintf("video-%s.mp4", testID) + + // Create media with VideoWeb purpose + media := &models.Media{ + Title: fmt.Sprintf("Test Video %s", testID), + Path: filepath.Join(t.TempDir(), mediaName), + PathHash: fmt.Sprintf("testhash-%s", testID), + AlbumID: album.ID, + Album: *album, + DateShot: time.Now(), + Type: "video", + } + require.NoError(t, db.Create(media).Error) + t.Cleanup(func() { + db.Unscoped().Delete(media) + }) + + // Create media URL entry + mediaURL := &models.MediaURL{ + MediaID: media.ID, + Media: media, + MediaName: mediaName, + Width: 1920, + Height: 1080, + Purpose: models.VideoWeb, + ContentType: "video/mp4", + FileSize: 1024, + } + require.NoError(t, db.Create(mediaURL).Error) + t.Cleanup(func() { + db.Unscoped().Delete(mediaURL) + }) + + // Create a unique cache path for this test + cachePath := filepath.Join(t.TempDir(), fmt.Sprintf("cache-%s", testID)) + require.NoError(t, os.MkdirAll(cachePath, 0755)) + t.Cleanup(func() { + os.RemoveAll(cachePath) + }) + + // Prepare share token for auth tests + tokenPassword := fmt.Sprintf("secret-password-%s", testID) + expiry := time.Now().Add(24 * time.Hour) + shareToken, err := actions.AddMediaShare(db, user, media.ID, &expiry, &tokenPassword) + require.NoError(t, err) + t.Cleanup(func() { + db.Unscoped().Delete(shareToken) + }) + + return user, album, media, mediaURL, mediaName, cachePath, shareToken.Value, tokenPassword +} + +func TestVideoRoutes(t *testing.T) { + // Ensure original function is always restored + defer func() { + processSingleMediaFn = originalProcessSingleMedia + }() + + // Setup test database + db := test_utils.DatabaseTest(t) + + // Define test cases + testCases := []struct { + name string + testFunc func(*testing.T, *gorm.DB) + }{ + { + name: "Valid video retrieval", + testFunc: func(t *testing.T, db *gorm.DB) { + // Create unique resources for this test + _, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "valid") + + // Setup cache path for this test + restorePath := setTestCachePath(cachePath) + t.Cleanup(restorePath) + + // Create cache directory and file + albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID))) + mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID))) + require.NoError(t, os.MkdirAll(mediaDir, 0755)) + + videoPath := filepath.Join(mediaDir, mediaName) + require.NoError(t, os.WriteFile(videoPath, []byte("test video content"), 0644)) + + // Create mock router without auth for this test + router := mux.NewRouter() + registerMockVideoRoutesForTesting(db, router, cachePath) + + // Make request + req := httptest.NewRequest("GET", "/"+mediaName, nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + // Validate response + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "test video content", rr.Body.String()) + }, + }, + { + name: "Video not found", + testFunc: func(t *testing.T, db *gorm.DB) { + // Create unique resources for this test + _, _, _, _, _, cachePath, _, _ := createTestResources(t, db, "notfound") + + // Setup cache path for this test + restorePath := setTestCachePath(cachePath) + t.Cleanup(restorePath) + + // Create mock router without auth for this test + router := mux.NewRouter() + registerMockVideoRoutesForTesting(db, router, cachePath) + + // Make request with nonexistent video name + req := httptest.NewRequest("GET", "/nonexistent.mp4", nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + // Validate response + assert.Equal(t, http.StatusNotFound, rr.Code) + assert.Equal(t, "not found", rr.Body.String()) + }, + }, + { + name: "Authentication with share token", + testFunc: func(t *testing.T, db *gorm.DB) { + // Create unique resources for this test + _, album, media, _, mediaName, cachePath, tokenValue, tokenPassword := createTestResources(t, db, "auth") + + // Setup cache path for this test + restorePath := setTestCachePath(cachePath) + t.Cleanup(restorePath) + + // Create the file in cache + albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID))) + mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID))) + require.NoError(t, os.MkdirAll(mediaDir, 0755)) + + videoPath := filepath.Join(mediaDir, mediaName) + require.NoError(t, os.WriteFile(videoPath, []byte("test video content"), 0644)) + + // Create real router with auth for this test + router := mux.NewRouter() + RegisterVideoRoutes(db, router) + + // Make request with token + req := httptest.NewRequest("GET", "/"+mediaName+"?token="+tokenValue, nil) + cookie := http.Cookie{ + Name: fmt.Sprintf("share-token-pw-%s", tokenValue), + Value: tokenPassword, + } + req.AddCookie(&cookie) + + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + // Validate response + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "test video content", rr.Body.String()) + }, + }, + { + name: "Multiple media URLs with same name", + testFunc: func(t *testing.T, db *gorm.DB) { + // Create unique resources for this test + _, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "multiple") + + // Setup cache path for this test + restorePath := setTestCachePath(cachePath) + t.Cleanup(restorePath) + + // Create second mediaURL with same name + mediaURL2 := &models.MediaURL{ + MediaID: media.ID, + Media: media, + MediaName: mediaName, // Same name + Width: 1280, + Height: 720, + Purpose: models.VideoWeb, + ContentType: "video/mp4", + FileSize: 512, + } + require.NoError(t, db.Create(mediaURL2).Error) + t.Cleanup(func() { + db.Unscoped().Delete(mediaURL2) + }) + + // Create cache directory and file + albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID))) + mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID))) + require.NoError(t, os.MkdirAll(mediaDir, 0755)) + + videoPath := filepath.Join(mediaDir, mediaName) + require.NoError(t, os.WriteFile(videoPath, []byte("test video content"), 0644)) + + // Create mock router without auth for this test + router := mux.NewRouter() + registerMockVideoRoutesForTesting(db, router, cachePath) + + // Make request + req := httptest.NewRequest("GET", "/"+mediaName, nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + // Validate response + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "test video content", rr.Body.String()) + }, + }, + { + name: "Video file not in cache, processing succeeds", + testFunc: func(t *testing.T, db *gorm.DB) { + // Create unique resources for this test + _, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "process-success") + + // Setup cache path for this test + restorePath := setTestCachePath(cachePath) + t.Cleanup(restorePath) + + // Ensure cache directory exists but file doesn't exist + albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID))) + mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID))) + require.NoError(t, os.MkdirAll(mediaDir, 0755)) + + // Mock processing to succeed + restoreProcessingFn := mockProcessSingleMedia(t, true, int(media.ID), int(album.ID)) + t.Cleanup(restoreProcessingFn) + + // Create mock router without auth for this test + router := mux.NewRouter() + registerMockVideoRoutesForTesting(db, router, cachePath) + + // Make request + req := httptest.NewRequest("GET", "/"+mediaName, nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + // Validate response + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "mocked processed video content", rr.Body.String()) + }, + }, + { + name: "Video file not in cache, processing fails", + testFunc: func(t *testing.T, db *gorm.DB) { + // Create unique resources for this test + _, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "process-fail") + + // Setup cache path for this test + restorePath := setTestCachePath(cachePath) + t.Cleanup(restorePath) + + // Ensure cache directory exists but file doesn't exist + albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID))) + mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID))) + require.NoError(t, os.MkdirAll(mediaDir, 0755)) + + // Mock processing to fail + restoreProcessingFn := mockProcessSingleMedia(t, false, int(media.ID), int(album.ID)) + t.Cleanup(restoreProcessingFn) + + // Create mock router without auth for this test + router := mux.NewRouter() + registerMockVideoRoutesForTesting(db, router, cachePath) + + // Make request + req := httptest.NewRequest("GET", "/"+mediaName, nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + // Validate response + assert.Equal(t, http.StatusInternalServerError, rr.Code) + }, + }, + { + name: "Context cancellation during processing", + testFunc: func(t *testing.T, db *gorm.DB) { + _, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "cancellation") + + // Setup cache path for this test + restorePath := setTestCachePath(cachePath) + t.Cleanup(restorePath) + + // Ensure cache directory exists but file doesn't exist to trigger processing + albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID))) + mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID))) + require.NoError(t, os.MkdirAll(mediaDir, 0755)) + + // Create cancellable context + ctx, cancel := context.WithCancel(context.Background()) + + // Mock processing that simulates context cancellation + savedFn := processSingleMediaFn + processSingleMediaFn = func(reqCtx context.Context, db *gorm.DB, media *models.Media) error { + cancel() + return fmt.Errorf("processing interrupted by cancellation") + } + t.Cleanup(func() { processSingleMediaFn = savedFn }) + + // Create request with cancelled context + req := httptest.NewRequest("GET", "/video/"+mediaName, nil) + req = req.WithContext(ctx) + w := httptest.NewRecorder() + + // Use testing router without auth + mockRouter := mux.NewRouter().PathPrefix("/video").Subrouter() + registerMockVideoRoutesForTesting(db, mockRouter, cachePath) + + mockRouter.ServeHTTP(w, req) + + // When context is cancelled, processing should be cancelled + // 1. Status remains default 200 (no explicit status written) + assert.Equal(t, http.StatusOK, w.Code, "Status should remain default when context cancelled") + + // 2. Response body should be empty (no video content served) + assert.Empty(t, w.Body.String(), "Response body should be empty when context cancelled") + }, + }, + } + + // Run test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.testFunc(t, db) + }) + } +} diff --git a/api/scanner/externaltools/exif/exif.go b/api/scanner/externaltools/exif/exif.go new file mode 100644 index 0000000..5982a8c --- /dev/null +++ b/api/scanner/externaltools/exif/exif.go @@ -0,0 +1,115 @@ +package exif + +import ( + "fmt" + "sync" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/externaltools/exiftool" +) + +var globalExifParser *exiftool.Exiftool +var globalInit sync.Once + +func Initialize() (func(), error) { + var err error + globalInit.Do(func() { + globalExifParser, err = exiftool.New() + }) + + if err != nil { + return nil, err + } + + log.Info(nil, "Found exiftool.", "binary_path", globalExifParser.BinaryPath(), "version", globalExifParser.Version()) + + return func() { + globalMu.Lock() + defer globalMu.Unlock() + + if globalExifParser == nil { + return + } + + if err := globalExifParser.Close(); err != nil { + log.Error(nil, "Cleanup exiftool error", "error", err) + return + } + globalExifParser = nil + }, nil +} + +var globalMu sync.Mutex + +func Parse(filepath string) (*models.MediaEXIF, error) { + globalMu.Lock() + defer globalMu.Unlock() + + if globalExifParser == nil { + return nil, fmt.Errorf("no exif parser initialized") + } + + var values struct { + exiftool.PhotoMeta + exiftool.TimeAll + exiftool.GPS + } + if err := globalExifParser.QueryJSONTagsByNumber(filepath, &values); err != nil { + return nil, err + } + + values.PhotoMeta.SanitizeFloats() + + ret := models.MediaEXIF{ + Camera: values.Model, + Maker: values.Make, + Lens: values.LensModel, + Iso: values.ISO, + Flash: values.Flash, + Orientation: values.Orientation, + ExposureProgram: values.ExposureProgram, + Exposure: values.ExposureTime, + Aperture: values.Aperture, + FocalLength: values.FocalLength, + Description: values.ImageDescription, + } + + dateShot := values.TimeAll.TimeInLocal() + if !dateShot.IsZero() { + ret.DateShot = new(dateShot) + } + + offsetSec, ok := values.TimeAll.OffsetSecs(dateShot) + if ok { + ret.OffsetSecShot = &offsetSec + } + + if values.GPS.IsValid() { + ret.GPSLatitude = values.GPS.GPSLatitude + ret.GPSLongitude = values.GPS.GPSLongitude + } + + return &ret, nil +} + +func MIMEType(filepath string) (string, error) { + globalMu.Lock() + defer globalMu.Unlock() + + if globalExifParser == nil { + return "", fmt.Errorf("no exif parser initialized") + } + + var mime exiftool.MIMEType + if err := globalExifParser.QueryJSONTagsByNumber(filepath, &mime); err != nil { + return "", err + } + + if mime.MIMEType == nil { + return "", nil + } + + return *mime.MIMEType, nil + +} diff --git a/api/scanner/externaltools/exif/exif_test.go b/api/scanner/externaltools/exif/exif_test.go new file mode 100644 index 0000000..f073af6 --- /dev/null +++ b/api/scanner/externaltools/exif/exif_test.go @@ -0,0 +1,141 @@ +package exif + +import ( + "os" + "sync" + "testing" + "time" + + _ "github.com/photoview/photoview/api/test_utils/flags" +) + +func resetForTest() { + globalMu.Lock() + defer globalMu.Unlock() + if globalExifParser != nil { + _ = globalExifParser.Close() + } + globalExifParser = nil + // Allow Initialize() to run again in subsequent tests + globalInit = sync.Once{} +} + +func TestParseWithoutInit(t *testing.T) { + resetForTest() + if _, err := Parse("./test_data/bird.jpg"); err == nil { + t.Fatalf("Parse() without Init() doesn't return an error") + } +} + +func TestParse(t *testing.T) { + resetForTest() + + cleanup, err := Initialize() + if err != nil { + t.Fatalf("Initialize() error: %v", err) + } + defer cleanup() + + filename := "./test_data/bird.jpg" + + metadata, err := Parse(filename) + if err != nil { + t.Fatalf("Parse() returns an error: %v", err) + } + + if metadata == nil { + t.Errorf("Parse(%q) should not return nil", filename) + } +} + +func TestMIMEType(t *testing.T) { + resetForTest() + + cleanup, err := Initialize() + if err != nil { + t.Fatalf("Initialize() error: %v", err) + } + defer cleanup() + + filename := "./test_data/bird.jpg" + + mime, err := MIMEType(filename) + if err != nil { + t.Fatalf("MIMEType() returns an error: %v", err) + } + + if mime == "" { + t.Errorf("MIMEType(%q) should not return an empty string", filename) + } +} + +func fileModifyDateLiteralInUTC(t *testing.T, file string) time.Time { + fstat, err := os.Stat(file) + if err != nil { + t.Fatalf("os.Stat(%q) error: %v", file, err) + } + + ret := fstat.ModTime().Truncate(time.Second) + _, offset := ret.Zone() + ret = ret.Add(time.Duration(offset) * time.Second).UTC() + + return ret +} + +func mustParseNoTimeZone(t *testing.T, timeStr string) time.Time { + layout := "2006:01:02 15:04:05.999" + ret, err := time.ParseInLocation(layout, timeStr, time.UTC) + if err != nil { + t.Fatalf("time.Parse(%q) error: %v", timeStr, err) + } + return ret +} + +func TestSamplesTime(t *testing.T) { + resetForTest() + + cleanup, err := Initialize() + if err != nil { + t.Fatalf("Initialize() error: %v", err) + } + defer cleanup() + + tests := []struct { + file string + wantTime time.Time + wantOffsetSec *int + }{ + {"./test_data/sample1.heif", fileModifyDateLiteralInUTC(t, "./test_data/sample1.heif"), nil}, + {"./test_data/sample1_nef.jpg", mustParseNoTimeZone(t, "2008:03:15 07:44:21.49"), new(-7 * 60 * 60)}, + } + + for _, tc := range tests { + t.Run(tc.file, func(t *testing.T) { + metadata, err := Parse(tc.file) + if err != nil { + t.Fatalf("Parse(%q) returns an error: %v", tc.file, err) + } + + if metadata == nil { + t.Fatalf("metadata == nil") + } + if metadata.DateShot == nil { + t.Fatalf("metadata.DateShot == nil") + } + + if got := *metadata.DateShot; !got.Equal(tc.wantTime) { + t.Errorf("metadata.DateShot = %v, want: %v", got, tc.wantTime) + } + + if tc.wantOffsetSec == nil { + if got := metadata.OffsetSecShot; got != nil { + t.Errorf("metadata.OffsetSecShot = %+v, want: nil", got) + } + } else { + if got, want := *metadata.OffsetSecShot, *tc.wantOffsetSec; got != want { + t.Errorf("metadata.OffsetSecShot = &(%v), want: &(%v)", got, want) + } + } + }) + } +} diff --git a/api/scanner/externaltools/exiftool/exiftool.go b/api/scanner/externaltools/exiftool/exiftool.go new file mode 100644 index 0000000..fc7d10e --- /dev/null +++ b/api/scanner/externaltools/exiftool/exiftool.go @@ -0,0 +1,260 @@ +package exiftool + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os/exec" + "sync" +) + +// Exiftool launches an external `exiftool` process to query photos' exif info. +// It doesn't support concurrency usage. +type Exiftool struct { + path string + version string + marker string + + cmd *exec.Cmd + stdin io.WriteCloser + stdinBuf *bufio.Writer + stdout *MarkReader + stderr *MarkReader + + closeOnce sync.Once +} + +const marker = "{ready}\n" +const bufferSize = 10240 + +// New returns a new instance of Exiftool. +func New() (*Exiftool, error) { + path, err := exec.LookPath("exiftool") + if err != nil { + return nil, err + } + + voutput, err := exec.Command(path, "-ver").Output() + if err != nil { + return nil, fmt.Errorf("run `exiftool -ver` error: %w", err) + } + + version := string(bytes.Trim(voutput, " \t\n\r")) + if version == "" { + return nil, fmt.Errorf("run `exiftool -ver` error: no output") + } + + cmd := exec.Command(path, "-stay_open", "True", "-@", "-") + + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, fmt.Errorf("get `exiftool -stay_open True -@ -` stdin error: %w", err) + } + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + defer stdin.Close() + return nil, fmt.Errorf("get `exiftool -stay_open True -@ -` stdout error: %w", err) + } + + stdout, err := NewMarkReader(stdoutPipe, bufferSize, marker) + if err != nil { + defer stdin.Close() + return nil, err + } + + stderrPipe, err := cmd.StderrPipe() + if err != nil { + defer stdin.Close() + return nil, fmt.Errorf("get `exiftool -stay_open True -@ -` stderr error: %w", err) + } + + stderr, err := NewMarkReader(stderrPipe, bufferSize, marker) + if err != nil { + defer stdin.Close() + return nil, err + } + + if err := cmd.Start(); err != nil { + defer stdin.Close() + return nil, fmt.Errorf("run `exiftool -stay_open True -@ -` error: %w", err) + } + + return &Exiftool{ + path: path, + version: version, + marker: marker, + cmd: cmd, + stdin: stdin, + stdinBuf: bufio.NewWriterSize(stdin, bufferSize), + stdout: stdout, + stderr: stderr, + }, nil +} + +// BinaryPath returns the path of `exiftool` binary. +func (e *Exiftool) BinaryPath() string { + return e.path +} + +// Version returns the version of `exiftool` binary. +func (e *Exiftool) Version() string { + return e.version +} + +// Close stops the external process of `exiftool`. +func (e *Exiftool) Close() (err error) { + e.closeOnce.Do(func() { + defer e.cmd.Wait() + defer e.stdin.Close() + + if err = e.rawSendCommand("-stay_open", "False"); err != nil { + _ = e.cmd.Process.Kill() + } + }) + + return +} + +func (e *Exiftool) rawSendCommand(args ...string) error { + e.stdout.Reset() + e.stderr.Reset() + + for _, arg := range args { + if _, err := e.stdinBuf.WriteString(arg); err != nil { + return err + } + if err := e.stdinBuf.WriteByte('\n'); err != nil { + return err + } + } + + for _, arg := range []string{"-echo4\n", "{ready}\n", "-execute\n"} { + if _, err := e.stdinBuf.WriteString(arg); err != nil { + return err + } + } + + return e.stdinBuf.Flush() +} + +func (e *Exiftool) rawReadStderr() error { + stderrOutput, err := io.ReadAll(e.stderr) + if err != nil { + return err + } + + outStr := string(bytes.Trim(stderrOutput, " \n\t\r")) + if outStr == "" { + return nil + } + + return errors.New(outStr) +} + +func (e *Exiftool) rawGetTags(v any, args ...string) (err error) { + if err = e.rawSendCommand(append(args, "-json")...); err != nil { + return + } + defer func() { + _, _ = io.Copy(io.Discard, e.stdout) + err = e.rawReadStderr() + }() + + if err = json.NewDecoder(e.stdout).Decode(v); err != nil { + return + } + + return nil +} + +func (e *Exiftool) rawSaveEmbedFile(outputPath string, args ...string) (hasEmbededFile bool, err error) { + if err = e.rawSendCommand(append(args, "-b", "-W", outputPath)...); err != nil { + return + } + defer func() { + _, _ = io.Copy(io.Discard, e.stdout) + err = e.rawReadStderr() + }() + + var output []byte + if output, err = io.ReadAll(e.stdout); err != nil { + return + } + + outStr := string(bytes.Trim(output, " \n\r\t")) + switch outStr { + case "0 output files created": + return + case "1 output files created": + default: + err = fmt.Errorf("invalid output: %s", outStr) + return + } + + if err = e.rawReadStderr(); err != nil { + return + } + + hasEmbededFile = true + return +} + +func (e *Exiftool) rawUpdateFile(args ...string) (err error) { + if err = e.rawSendCommand(args...); err != nil { + return + } + defer func() { + _, _ = io.Copy(io.Discard, e.stdout) + err = e.rawReadStderr() + }() + + var output []byte + if output, err = io.ReadAll(e.stdout); err != nil { + return + } + + outStr := string(bytes.Trim(output, " \n\r\t")) + if outStr != "1 image files updated" { + err = fmt.Errorf("invalid output: %s", outStr) + return + } + + return +} + +// QueryJSONTagsByNumber queries the exif info of `file` with a given struct `value`. Tags are fields of the `value`. The values are a number if possible. +// See values.go for example structs. +func (e *Exiftool) QueryJSONTagsByNumber(file string, value any) error { + rows := []any{value} + if err := e.rawGetTags(&rows, "-n", file); err != nil { + return fmt.Errorf("query %q tags error: %w", file, err) + } + + if len(rows) != 1 { + return fmt.Errorf("query %q tags error: return %d responses, should be only 1", file, len(rows)) + } + + return nil +} + +// SaveJPEGPreview saves a preview jpeg from `src` to `previewOutput`. +func (e *Exiftool) SaveJPEGPreview(src string, previewOutput string) (bool, error) { + saved, err := e.rawSaveEmbedFile(previewOutput, "-JpgFromRaw", src) + if err != nil { + return false, fmt.Errorf("save jpeg preview for %q error: %w", src, err) + } + + if !saved { + return false, nil + } + + if err = e.rawUpdateFile("-TagsFromFile", src, "-overwrite_original", previewOutput); err != nil { + return false, fmt.Errorf("save tags to jpeg preview for %q error: %w", src, err) + } + + return true, nil +} diff --git a/api/scanner/externaltools/exiftool/exiftool_test.go b/api/scanner/externaltools/exiftool/exiftool_test.go new file mode 100644 index 0000000..1e380e6 --- /dev/null +++ b/api/scanner/externaltools/exiftool/exiftool_test.go @@ -0,0 +1,409 @@ +package exiftool + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + _ "github.com/photoview/photoview/api/test_utils/flags" +) + +func TestExiftool(t *testing.T) { + instance, err := New() + if err != nil { + t.Fatalf("create instance error: %v", err) + } + + t.Log("bin:", instance.BinaryPath()) + t.Log("version:", instance.Version()) + + if instance.BinaryPath() == "" { + t.Errorf("want exiftool binary, but got an empty string") + } + + if instance.Version() == "" { + t.Errorf("want exiftool version, but got an emtpy string") + } + + if err := instance.Close(); err != nil { + t.Errorf("close instance error: %v", err) + } +} + +func TestExiftoolQueryJSONTagsWithEmbed(t *testing.T) { + instance, err := New() + if err != nil { + t.Fatalf("new error: %v", err) + } + defer instance.Close() + + file := "./test_data/correct_gps.jpg" + var value struct { + TimeAll + MIMEType + PhotoMeta + } + + if err := instance.QueryJSONTagsByNumber(file, &value); err != nil { + t.Fatalf("QueryJSONTagsByNumber(%q) error: %v", file, err) + return + } + + if time := value.TimeAll.TimeInLocal(); time.IsZero() { + t.Errorf("QueryJSONTagsByNumber(%q) error: no valid TimeAll", file) + } + if value.MIMEType.MIMEType == nil { + t.Errorf("QueryJSONTagsByNumber(%q) error: no valid MIMEType", file) + } + if value.PhotoMeta.Model == nil { + t.Errorf("QueryJSONTagsByNumber(%q) error: no valid PhotoMeta", file) + } +} + +func TestExiftoolQueryMIMEType(t *testing.T) { + tests := []struct { + file string + want string + }{ + {"./test_data/no_timezone.jpg", "image/jpeg"}, + {"./test_data/subsec_timezone.heic", "image/heic"}, + {"./test_data/raw_with_preview_jpg.cr3", "image/x-canon-cr3"}, + {"./test_data/no_exif.jpg", "image/jpeg"}, + } + + instance, err := New() + if err != nil { + t.Fatalf("new error: %v", err) + } + defer instance.Close() + + for _, tc := range tests { + t.Run(tc.file, func(t *testing.T) { + var value struct{ MIMEType } + err := instance.QueryJSONTagsByNumber(tc.file, &value) + if err != nil { + t.Errorf("QueryJSONTagsByNumber(%q) error: %v", tc.file, err) + return + } + + if got := value.MIMEType.MIMEType; got == nil || *got != tc.want { + t.Errorf("QueryJSONTagsByNumber(%q) = %v, want: %q", tc.file, got, tc.want) + } + }) + } +} + +func checkTimeallFieldsHasValue(t *testing.T, time TimeAll, fields []string) { + t.Helper() + + tv := reflect.ValueOf(time) + + for _, field := range fields { + fv := tv.FieldByName(field) + if !fv.IsValid() { + t.Errorf("can't find field %q in TimeAll", field) + continue + } + + if fv.IsNil() || fv.Elem().IsZero() { + t.Errorf("field %q is type %T with value %v", field, fv.Type(), fv.Interface()) + continue + } + } +} + +func TestExiftoolQueryTimeAllHasOffset(t *testing.T) { + tests := []struct { + file string + wantKeys []string + wantTime time.Time + wantOffsetSec int + }{ + {"./test_data/raw_with_preview_jpg.cr3", []string{ + "CreateDate", + "DateTimeOriginal", + "FileModifyDate", + "MediaCreateDate", + "OffsetTime", + "OffsetTimeOriginal", + "SubSecCreateDate", + "SubSecDateTimeOriginal", + "TimeZone", + "TrackCreateDate", + }, mustParseInUTC(t, "2019:09:13 14:36:48.87"), 7200}, + {"./test_data/subsec_timezone.heic", []string{ + "CreateDate", + "DateTimeOriginal", + "FileModifyDate", + "OffsetTime", + "OffsetTimeOriginal", + "SubSecCreateDate", + "SubSecDateTimeOriginal", + }, mustParseInUTC(t, "2025:10:28 14:20:22.164"), 3600}, + {"./test_data/createdate_timezone_separate.jpg", []string{ + "CreateDate", + "DateTimeOriginal", + "TimeZone", + "FileModifyDate", + "SubSecCreateDate", + "SubSecDateTimeOriginal", + }, mustParseInUTC(t, "2008:03:15 07:44:21.49"), -7 * 60 * 60}, + } + + instance, err := New() + if err != nil { + t.Fatalf("new error: %v", err) + } + defer instance.Close() + + for _, tc := range tests { + t.Run(tc.file, func(t *testing.T) { + var value struct{ TimeAll } + err := instance.QueryJSONTagsByNumber(tc.file, &value) + if err != nil { + t.Errorf("QueryJSONTagsByNumber(%q) error: %v", tc.file, err) + return + } + + checkTimeallFieldsHasValue(t, value.TimeAll, tc.wantKeys) + + gotTime := value.TimeAll.TimeInLocal() + if !gotTime.Equal(tc.wantTime) { + t.Errorf("value.TimeAll.TimeInLocal() = %v, want: %v", gotTime, tc.wantTime) + } + + if got, ok := value.TimeAll.OffsetSecs(gotTime); !ok || got != tc.wantOffsetSec { + t.Errorf("value.TimeAll.OffsetSecs() = (%v, %v), want: (%v, true)", got, ok, tc.wantOffsetSec) + } + }) + } +} + +func fileModifyDateLiteralUTC(t *testing.T, file string) time.Time { + t.Helper() + + dir := filepath.Dir(file) + file = filepath.Base(file) + + fsys := os.DirFS(dir) + info, err := fs.Stat(fsys, file) + if err != nil { + t.Fatalf("read file stat error: %v", err) + } + + ret := info.ModTime().Truncate(time.Second) + + _, offsetSec := ret.Zone() + ret = ret.Add(time.Duration(offsetSec) * time.Second).UTC() + + return ret +} + +func TestExiftoolQueryTimeAllNoOffset(t *testing.T) { + tests := []struct { + file string + wantKeys []string + wantTime time.Time + wantHasOffset bool + wantOffset int + }{ + {"./test_data/no_timezone.jpg", []string{ + "DateTimeOriginal", + "FileModifyDate", + }, mustParseInUTC(t, "2012:05:06 15:39:44"), false, 0}, + {"./test_data/subsec_no_timezone.heic", []string{ + "CreateDate", + "DateTimeOriginal", + "FileModifyDate", + "OffsetTime", + "SubSecCreateDate", + "SubSecDateTimeOriginal", + }, mustParseInUTC(t, "2025:10:28 14:20:22.164"), true, 3600}, + {"./test_data/no_exif.jpg", []string{ + "FileModifyDate", + }, fileModifyDateLiteralUTC(t, "./test_data/no_exif.jpg"), false, 0}, + } + + instance, err := New() + if err != nil { + t.Fatalf("new error: %v", err) + } + defer instance.Close() + + for _, tc := range tests { + t.Run(tc.file, func(t *testing.T) { + var value struct{ TimeAll } + err := instance.QueryJSONTagsByNumber(tc.file, &value) + if err != nil { + t.Errorf("QueryJSONTagsByNumber(%q) error: %v", tc.file, err) + return + } + + checkTimeallFieldsHasValue(t, value.TimeAll, tc.wantKeys) + + gotTime := value.TimeAll.TimeInLocal() + if !gotTime.Equal(tc.wantTime) { + t.Errorf("value.TimeAll.TimeInLocal() = %v, want: %v", gotTime, tc.wantTime) + } + + gotOffset, gotHasOffset := value.TimeAll.OffsetSecs(gotTime) + if gotHasOffset != tc.wantHasOffset { + t.Errorf("value.TimeAll.OffsetSecs() = (_, %v), want: (_, %v)", gotHasOffset, tc.wantHasOffset) + } + if !gotHasOffset { + return + } + if gotOffset != tc.wantOffset { + t.Errorf("value.TimeAll.OffsetSecs() = (%v, _), want: (%v, _)", gotOffset, tc.wantHasOffset) + } + }) + } +} + +func TestExiftoolQueryGPS(t *testing.T) { + tests := []struct { + file string + hasGPS bool + wantLat, wantLong float64 + }{ + {"./test_data/correct_gps.jpg", true, 44.4789972, 11.2979222}, + {"./test_data/incorrect_gps.jpg", false, 0, 0}, + {"./test_data/no_exif.jpg", false, 0, 0}, + } + + instance, err := New() + if err != nil { + t.Fatalf("new error: %v", err) + } + defer instance.Close() + + for _, tc := range tests { + t.Run(tc.file, func(t *testing.T) { + var value struct{ GPS } + if err := instance.QueryJSONTagsByNumber(tc.file, &value); err != nil { + t.Errorf("QueryJSONTagsByNumber(%q) error: %v", tc.file, err) + return + } + + hasGPS := value.GPS.IsValid() + if hasGPS != tc.hasGPS { + t.Errorf("QueryJSONTagsByNumber(%q) has GPS: %v, want GPS: %v", tc.file, hasGPS, tc.hasGPS) + return + } + if !tc.hasGPS { + return + } + + gpsToString := func(latitude, longitude float64) string { + return fmt.Sprintf("(%.7f, %.7f)", latitude, longitude) + } + + lat := *value.GPS.GPSLatitude + long := *value.GPS.GPSLongitude + if got, want := gpsToString(lat, long), gpsToString(tc.wantLat, tc.wantLong); got != want { + t.Errorf("QueryJSONTagsByNumber(%q) = %s, want: %s", tc.file, got, want) + } + }) + } +} + +func TestExiftoolSaveJPEGPreview(t *testing.T) { + tests := []struct { + file string + wantOK bool + }{ + {"./test_data/raw_with_preview_jpg.cr3", true}, + {"./test_data/no_timezone.jpg", false}, + } + + instance, err := New() + if err != nil { + t.Fatalf("new error: %v", err) + } + defer instance.Close() + + outputDir := t.TempDir() + + for _, tc := range tests { + t.Run(tc.file, func(t *testing.T) { + output := filepath.Join(outputDir, "preview.jpg") + ok, err := instance.SaveJPEGPreview(tc.file, output) + if err != nil { + t.Errorf("SaveJPEGPreview(%q, %q) error: %v", tc.file, output, err) + return + } + + if ok != tc.wantOK { + t.Errorf("SaveJPEGPreview(%q, %q) = %v, want: %v", tc.file, output, ok, tc.wantOK) + } + + if !ok { + return + } + + var jpg struct { + MIMEType + TimeAll + } + if err := instance.QueryJSONTagsByNumber(output, &jpg); err != nil { + t.Fatalf("QueryJSONTagsByNumber(%q) error: %v", output, err) + return + } + + if got, want := jpg.MIMEType.MIMEType, "image/jpeg"; got == nil || *got != want { + t.Errorf("MIMEType(%q) = %v, want: %q", output, got, want) + return + } + + var raw struct{ TimeAll } + if err := instance.QueryJSONTagsByNumber(tc.file, &raw); err != nil { + t.Fatalf("QueryJSONTagsByNumber(%q) error: %v", tc.file, err) + } + + if got, want := jpg.TimeAll.TimeInLocal(), raw.TimeAll.TimeInLocal(); !got.Equal(want) { + t.Errorf("jpg.TimeAll.TimeInLocal() = %q, want: %q", got, want) + } + }) + } + +} + +func TestExiftoolError(t *testing.T) { + instance, err := New() + if err != nil { + t.Fatalf("new error: %v", err) + } + defer instance.Close() + + tests := []struct { + file string + errStr string + }{ + {"./test_data/non_exist.jpg", "File not found"}, + } + + for _, tc := range tests { + t.Run(tc.file, func(t *testing.T) { + checkErr := func(err error, fmtStr string, args ...any) { + t.Helper() + + if want := tc.errStr; err == nil || !strings.Contains(err.Error(), want) { + t.Errorf(fmtStr+" %v, want %v", append(args, err, want)...) + } + } + + var value MIMEType + err = instance.QueryJSONTagsByNumber(tc.file, &value) + checkErr(err, "QueryJSONTagsByNumber(%q)", tc.file) + + output := filepath.Join(t.TempDir(), "output.jpg") + _, err = instance.SaveJPEGPreview(tc.file, output) + checkErr(err, "SaveJPEGPreview(%q, %q)", tc.file, output) + }) + } +} diff --git a/api/scanner/externaltools/exiftool/mark_reader.go b/api/scanner/externaltools/exiftool/mark_reader.go new file mode 100644 index 0000000..da58668 --- /dev/null +++ b/api/scanner/externaltools/exiftool/mark_reader.go @@ -0,0 +1,116 @@ +package exiftool + +import ( + "bytes" + "errors" + "io" +) + +// MarkReader reads from upstream and stops at marker boundaries. +type MarkReader struct { + upstream io.Reader + buf []byte + mark []byte + + // valid data: buf[start:pending] + // pending, need to check if contains mark: buf[pending:end] + start int + pending int + end int + + // if true, mark begins from buf[pending] + hasMark bool + // if true, upstream is EOF + upstreamEOF bool + // if true, this reader returns EOF + paused bool +} + +// NewMarkReader creates a reader that stops at each marker and requires Reset to continue. +func NewMarkReader(upstream io.Reader, bufferSize int, mark string) (*MarkReader, error) { + if bufferSize < 2*len(mark) { + return nil, errors.New("buffer too small") + } + + return &MarkReader{ + upstream: upstream, + buf: make([]byte, bufferSize), + mark: []byte(mark), + }, nil +} + +// Reset resumes reading after a previously encountered marker boundary. +func (r *MarkReader) Reset() { + r.paused = false +} + +// Read returns bytes up to (but excluding) the next marker, then reports EOF until Reset. +func (r *MarkReader) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + if r.paused { + return 0, io.EOF + } + + if r.start == r.pending { + // compact buffer + copy(r.buf[0:], r.buf[r.start:r.end]) + r.end -= r.start + r.pending -= r.start + r.start = 0 + + // fill buffer + n, err := io.ReadAtLeast(r.upstream, r.buf[r.end:], len(r.mark)) + r.end += n + if err == io.EOF || err == io.ErrUnexpectedEOF { + r.upstreamEOF = true + err = nil + } + if err != nil { + return 0, err + } + + r.checkMarkInPending() + } + + // read from valid data + n := copy(p, r.buf[r.start:r.pending]) + r.start += n + + if r.start == r.pending { + switch { + case r.hasMark: + r.pending += len(r.mark) + r.start += len(r.mark) + r.hasMark = false + r.paused = true + + r.checkMarkInPending() + case r.upstreamEOF: + r.paused = r.start == r.end + } + } + + if r.paused { + return n, io.EOF + } + + return n, nil +} + +func (r *MarkReader) checkMarkInPending() { + if markAt := bytes.Index(r.buf[r.pending:r.end], r.mark); markAt >= 0 { + r.hasMark = true + r.pending = r.pending + markAt + return + } + + if r.upstreamEOF { + r.pending = r.end + return + } + + r.pending = max(r.pending, r.end-len(r.mark)+1) +} diff --git a/api/scanner/externaltools/exiftool/mark_reader_test.go b/api/scanner/externaltools/exiftool/mark_reader_test.go new file mode 100644 index 0000000..5346e3c --- /dev/null +++ b/api/scanner/externaltools/exiftool/mark_reader_test.go @@ -0,0 +1,192 @@ +package exiftool + +import ( + "io" + "runtime" + "strings" + "testing" + "testing/iotest" +) + +func readAllString(t *testing.T, r io.Reader) string { + t.Helper() + + var data []byte + + for { + buf := make([]byte, 1024) + + var n int + var err error + + var memstats runtime.MemStats + runtime.ReadMemStats(&memstats) + mallocs := 0 - memstats.Mallocs + + n, err = r.Read(buf) + + runtime.ReadMemStats(&memstats) + mallocs += memstats.Mallocs + + if mallocs > 0 { + t.Errorf("r.Read() allocs %d bytes", mallocs) + } + + data = append(data, buf[:n]...) + + if err != nil { + if err == io.EOF { + break + } + + t.Fatalf("read failed: %v", err) + } + + if n == 0 { + t.Error("read error, return 0 byte without errors") + } + } + + return string(data) +} + +func TestNewMarkReader(t *testing.T) { + tests := []struct { + name string + bufferSize int + mark string + wantErr string + }{ + { + name: "BufferTooSmall", + bufferSize: 5, + mark: "abcd", + wantErr: "buffer too small", + }, + { + name: "TwiceMarkLen", + bufferSize: 8, + mark: "abcd", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := NewMarkReader(strings.NewReader("x"), tc.bufferSize, tc.mark) + if tc.wantErr == "" { + if err != nil { + t.Fatalf("NewMarkReader() returns error: %v", err) + } + return + } + + if err == nil || err.Error() != tc.wantErr { + t.Errorf("err = %v, want: %q", err, tc.wantErr) + } + }) + } +} + +func TestMarkReader(t *testing.T) { + tests := []struct { + name string + mark string + input string + bufferSize int + wantSegments []string + }{ + { + name: "CrossRead", + mark: "<>", + input: "hello<>world", + bufferSize: 2 * 8, /* len(mark) = 8 */ + wantSegments: []string{"hello", "world"}, + }, + { + name: "EndWithMark", + mark: "<>", + input: "foo<>bar<>", + bufferSize: 64, + wantSegments: []string{"foo", "bar"}, + }, + { + name: "TrailingPrefixMark", + mark: "<>", + input: "foo<>bar<>", + input: "<><>abc<>", + bufferSize: 64, + wantSegments: []string{"", "", "abc"}, + }, + { + name: "LongMark", + mark: "01234567890123456789", + input: "a01234567890123456789b", + bufferSize: 64, + wantSegments: []string{"a", "b"}, + }, + { + name: "RepeatMark", + mark: "aaaaa", /* a * 5 */ + input: "0aaaaa1aaa2aaaa3aaaaa4aaaaaa5", + bufferSize: 64, + wantSegments: []string{"0", "1aaa2aaaa3", "4", "a5"}, + }, + { + name: "OneByteMark", + mark: "|", + input: "0|1|234|5|6", + bufferSize: 64, + wantSegments: []string{"0", "1", "234", "5", "6"}, + }, + } + + readWraper := []struct { + name string + wrap func(r io.Reader) io.Reader + }{ + {"Original", func(r io.Reader) io.Reader { return r }}, + {"Half", iotest.HalfReader}, + {"OneByte", iotest.OneByteReader}, + } + + for _, wrapper := range readWraper { + t.Run(wrapper.name, func(t *testing.T) { + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + up := wrapper.wrap(strings.NewReader(tc.input)) + + r, err := NewMarkReader(up, tc.bufferSize, tc.mark) + if err != nil { + t.Fatal("NewMarkReader() returns error:", err) + } + + for i, want := range tc.wantSegments { + if got := readAllString(t, r); got != want { + t.Errorf("segment[%d] = %q, want: %q", i, got, want) + } + + var buf [1024]byte + if _, err := r.Read(buf[:]); err != io.EOF { + t.Error("r.Read() after readAllString() returns non-EOF error:", err) + } + + r.Reset() + } + + var buf [1024]byte + if _, err := r.Read(buf[:]); err != io.EOF { + t.Error("r.Read() after all segments returns non-EOF error:", err) + } + }) + } + }) + } +} diff --git a/api/scanner/externaltools/exiftool/values.go b/api/scanner/externaltools/exiftool/values.go new file mode 100644 index 0000000..78e3c8f --- /dev/null +++ b/api/scanner/externaltools/exiftool/values.go @@ -0,0 +1,171 @@ +package exiftool + +import ( + "fmt" + "math" + "strings" + "time" +) + +// GPS stores gps-related tags. +type GPS struct { + GPSLatitude *float64 + GPSLongitude *float64 +} + +// IsValid returns true when GPS data is valid. +func (gps GPS) IsValid() bool { + if gps.GPSLongitude == nil || gps.GPSLatitude == nil { + return false + } + + if math.IsNaN(*gps.GPSLatitude) { + return false + } + + if math.IsNaN(*gps.GPSLongitude) { + return false + } + + if math.Abs(*gps.GPSLatitude) > 90 || math.Abs(*gps.GPSLongitude) > 180 { + return false + } + + return true +} + +func (gps GPS) String() string { + if !gps.IsValid() { + return "GPS(invalid)" + } + + return fmt.Sprintf("GPS(%.9f, %.9f)", *gps.GPSLatitude, *gps.GPSLongitude) +} + +// TimeAll stores tags returned by -time:all. +type TimeAll struct { + SubSecDateTimeOriginal *string + SubSecCreateDate *string + + DateTimeOriginal *string + CreateDate *string + TrackCreateDate *string + MediaCreateDate *string + FileModifyDate *string + + OffsetTimeOriginal *string + OffsetTime *string + TimeZone *int + + GPSDateTime *string +} + +const layout = "2006:01:02 15:04:05.999" +const layoutWithTimezone = "2006:01:02 15:04:05.999Z07:00" + +// TimeInLocal returns most likely time. The date and time are in local. The timezone is meaningless and always be in UTC. Use `OffsetSecs()` to determine the timezone. +func (t TimeAll) TimeInLocal() time.Time { + for _, dateP := range []*string{ + // Keep the order for the priority to generate DateShot + t.SubSecDateTimeOriginal, + t.SubSecCreateDate, + t.DateTimeOriginal, + t.CreateDate, + t.TrackCreateDate, + t.MediaCreateDate, + t.FileModifyDate, + } { + if dateP == nil { + continue + } + + date := *dateP + + // Ignore timezone + if zoneIndex := strings.IndexAny(date, "+-Z"); zoneIndex >= 0 { + date = date[:zoneIndex] + } + + if date, err := time.ParseInLocation(layout, date, time.UTC); err == nil { + return date + } + } + + return time.Time{} +} + +// OffsetSecs returns seconds offset by UTC. +func (t TimeAll) OffsetSecs(local time.Time) (int, bool) { + for _, offsetP := range []*string{ + t.OffsetTimeOriginal, + t.OffsetTime, + } { + if offsetP == nil { + continue + } + + if t, err := time.Parse("-07:00", *offsetP); err == nil { + _, offsetSecs := t.Zone() + return offsetSecs, true + } + } + + // TimeZone is in minutes + if t.TimeZone != nil { + return *t.TimeZone * 60, true + } + + // Calculate offset sec with GPS time and local time. + if local.IsZero() { + return 0, false + } + + if t.GPSDateTime == nil { + return 0, false + } + + gpsDate, err := time.Parse(layoutWithTimezone, *t.GPSDateTime) + if err != nil { + return 0, false + } + gpsDate = gpsDate.UTC() + + // GPS time is always UTC per EXIF spec + // offset = local time (in UTC) - GPS UTC time + offset := int(local.Sub(gpsDate).Seconds()) + return offset, true +} + +type PhotoMeta struct { + ImageDescription *string + Model *string + Make *string + LensModel *string + ISO *int64 + Flash *int64 + Orientation *int64 + ExposureProgram *int64 + ExposureTime *float64 + Aperture *float64 + FocalLength *float64 +} + +func (m *PhotoMeta) SanitizeFloats() { + for _, value := range []**float64{ + &m.ExposureTime, + &m.Aperture, + &m.FocalLength, + } { + if *value == nil { + continue + } + + if math.IsNaN(**value) || math.IsInf(**value, 0) { + *value = nil + } + } +} + +type MIMEType struct { + MIMEType *string +} diff --git a/api/scanner/externaltools/exiftool/values_test.go b/api/scanner/externaltools/exiftool/values_test.go new file mode 100644 index 0000000..41e933b --- /dev/null +++ b/api/scanner/externaltools/exiftool/values_test.go @@ -0,0 +1,197 @@ +package exiftool + +import ( + "math" + "testing" + "time" +) + +func TestGPSIsValid(t *testing.T) { + tests := []struct { + name string + gps GPS + wantValid bool + }{ + {"LatNormalLongNormal", GPS{new(10.0), new(10.0)}, true}, + {"ZeroValue", GPS{new(0.0), new(0.0)}, true}, + + {"LatNilLongNormal", GPS{new(math.NaN()), new(10.0)}, false}, + {"LatNormalLongNil", GPS{new(10.0), new(math.NaN())}, false}, + + {"Lat>90LongNormal", GPS{new(100.0), new(10.0)}, false}, + {"Lat<-90LongNormal", GPS{new(-100.0), new(10.0)}, false}, + + {"LatNormalLong>180", GPS{new(10.0), new(190.0)}, false}, + {"LatNormalLong<-180", GPS{new(10.0), new(-190.0)}, false}, + + {"Empty", GPS{}, false}, + {"EmptyLat", GPS{nil, new(0.0)}, false}, + {"EmptyLong", GPS{new(0.0), nil}, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := tc.gps.IsValid() + if got != tc.wantValid { + t.Fatalf("gps.IsValid(%v) = %v, want: %v", tc.gps, got, tc.wantValid) + } + }) + } +} + +func mustParseInUTC(t *testing.T, timeStr string) time.Time { + t.Helper() + + ret, err := time.ParseInLocation(layout, timeStr, time.UTC) + if err != nil { + t.Fatalf("time.ParseInLocation(%q) returns error: %v", timeStr, err) + } + + return ret +} + +func TestTimeAllTimeLocal(t *testing.T) { + wantStr := "2025:10:28 14:20:22.164" + otherStr := "2024:10:28 14:20:22.164" + + tests := []struct { + name string + timeAll TimeAll + want time.Time + }{ + {"DateTimeOriginal", TimeAll{ + DateTimeOriginal: &wantStr, + CreateDate: &otherStr, + TrackCreateDate: &otherStr, + MediaCreateDate: &otherStr, + FileModifyDate: &otherStr, + }, mustParseInUTC(t, wantStr)}, + {"CreateDate", TimeAll{ + CreateDate: &wantStr, + TrackCreateDate: &otherStr, + MediaCreateDate: &otherStr, + FileModifyDate: &otherStr, + }, mustParseInUTC(t, wantStr)}, + {"TrackCreateDate", TimeAll{ + TrackCreateDate: &wantStr, + MediaCreateDate: &otherStr, + FileModifyDate: &otherStr, + }, mustParseInUTC(t, wantStr)}, + {"MediaCreateDate", TimeAll{ + MediaCreateDate: &wantStr, + FileModifyDate: &otherStr, + }, mustParseInUTC(t, wantStr)}, + {"FileModifyDate", TimeAll{ + FileModifyDate: &wantStr, + }, mustParseInUTC(t, wantStr)}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.timeAll.TimeInLocal(); !got.Equal(tc.want) { + t.Errorf("timeAll.Time() = %v, want: %v", got, tc.want) + } + }) + } +} + +func TestTimeAllTimeWithTimezone(t *testing.T) { + wantStr := "2025:10:28 14:20:22.164" + otherStr := "2024:10:28 14:20:22.164" + + tests := []struct { + name string + timeAll TimeAll + want time.Time + }{ + {"SubSecDateTimeOriginal", TimeAll{ + SubSecDateTimeOriginal: &wantStr, + SubSecCreateDate: &otherStr, + DateTimeOriginal: &otherStr, + CreateDate: &otherStr, + TrackCreateDate: &otherStr, + MediaCreateDate: &otherStr, + FileModifyDate: &otherStr, + }, mustParseInUTC(t, wantStr)}, + {"SubSecCreateDate", TimeAll{ + SubSecCreateDate: &wantStr, + DateTimeOriginal: &otherStr, + CreateDate: &otherStr, + TrackCreateDate: &otherStr, + MediaCreateDate: &otherStr, + FileModifyDate: &otherStr, + }, mustParseInUTC(t, wantStr)}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.timeAll.TimeInLocal(); !got.Equal(tc.want) { + t.Errorf("timeAll.Time() = %v, want: %v", got, tc.want) + } + }) + } +} + +func TestTimeAllTimeEmpty(t *testing.T) { + var timeAll TimeAll + if got := timeAll.TimeInLocal(); !got.IsZero() { + t.Errorf("timeAll.Time() is a valid time, which it should not be") + } +} + +func TestTimeAllOffsetSecs(t *testing.T) { + wantStr := "+01:00" + otherStr := "-01:00" + gpsDateTime := "2025:10:28 14:20:22Z" + otherTimezone := -120 + localTime := mustParseInUTC(t, "2025:10:28 13:20:22") + + tests := []struct { + name string + timeAll TimeAll + want int + }{ + {"OffsetTimeOriginal", TimeAll{ + OffsetTimeOriginal: &wantStr, + OffsetTime: &otherStr, + TimeZone: &otherTimezone, + GPSDateTime: &gpsDateTime, + }, 60 * 60}, + {"OffsetTime", TimeAll{ + OffsetTime: &wantStr, + TimeZone: &otherTimezone, + GPSDateTime: &gpsDateTime, + }, 60 * 60}, + {"TimeZone", TimeAll{ + TimeZone: &otherTimezone, + GPSDateTime: &gpsDateTime, + }, -120 * 60}, + {"GPS", TimeAll{ + GPSDateTime: &gpsDateTime, + }, -60 * 60}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, ok := tc.timeAll.OffsetSecs(localTime) + if !ok { + t.Fatalf("timeAll.OffsetSecs() is not valid, which it should be") + } + + if got != tc.want { + t.Errorf("timeAll.OffsetSecs() = %v, want: %v", got, tc.want) + } + }) + } +} + +func TestTimeAllOffsetSecsEmptyLocal(t *testing.T) { + timeAll := TimeAll{ + GPSDateTime: new("14:20:22 2025:10:28Z"), + } + + got, ok := timeAll.OffsetSecs(time.Time{}) + if ok || got != 0 { + t.Errorf("timeAll.OffsetSecs() = (%d, %v), want: (%d, %v)", got, ok, 0, false) + } +} diff --git a/api/scanner/externaltools/tools.go b/api/scanner/externaltools/tools.go new file mode 100644 index 0000000..75fd847 --- /dev/null +++ b/api/scanner/externaltools/tools.go @@ -0,0 +1,5 @@ +// Package externaltools provides wrappers for tools outside the Go runtime. +// These tools are provided by the runtime environment. Some require initialization +// and must be cleaned up properly when finished. +// Packages under externaltools should have as few dependencies as possible to avoid cycles. +package externaltools diff --git a/api/scanner/face_detection/face_detector.go b/api/scanner/face_detection/face_detector.go new file mode 100644 index 0000000..15a69ec --- /dev/null +++ b/api/scanner/face_detection/face_detector.go @@ -0,0 +1,17 @@ +package face_detection + +import ( + "gorm.io/gorm" + + "github.com/photoview/photoview/api/graphql/models" +) + +type FaceDetector interface { + ReloadFacesFromDatabase(db *gorm.DB) error + DetectFaces(db *gorm.DB, media *models.Media) error + MergeCategories(sourceID int32, destID int32) + MergeImageFaces(imageFaceIDs []int, destFaceGroupID int32) + RecognizeUnlabeledFaces(tx *gorm.DB, user *models.User) ([]*models.ImageFace, error) +} + +var GlobalFaceDetector FaceDetector = nil diff --git a/api/scanner/face_detection/face_detector_impl.go b/api/scanner/face_detection/face_detector_impl.go new file mode 100644 index 0000000..b80f327 --- /dev/null +++ b/api/scanner/face_detection/face_detector_impl.go @@ -0,0 +1,307 @@ +//go:build !no_face_detection + +package face_detection + +import ( + "log" + "sync" + + "github.com/Kagami/go-face" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type faceDetector struct { + mutex sync.Mutex + rec *face.Recognizer + faceDescriptors []face.Descriptor + faceGroupIDs []int32 + imageFaceIDs []int +} + +func InitializeFaceDetector(db *gorm.DB) error { + if utils.EnvDisableFaceRecognition.GetBool() { + log.Printf("Face detection disabled (%s=1)\n", utils.EnvDisableFaceRecognition.GetName()) + return nil + } + + log.Println("Initializing face detector") + + rec, err := face.NewRecognizer(utils.FaceRecognitionModelsPath()) + if err != nil { + return errors.Wrap(err, "initialize facedetect recognizer") + } + + faceDescriptors, faceGroupIDs, imageFaceIDs, err := getSamplesFromDatabase(db) + if err != nil { + return errors.Wrap(err, "get face detection samples from database") + } + + GlobalFaceDetector = &faceDetector{ + rec: rec, + faceDescriptors: faceDescriptors, + faceGroupIDs: faceGroupIDs, + imageFaceIDs: imageFaceIDs, + } + + return nil +} + +func getSamplesFromDatabase(db *gorm.DB) (samples []face.Descriptor, faceGroupIDs []int32, imageFaceIDs []int, err error) { + + var imageFaces []*models.ImageFace + + if err = db.Find(&imageFaces).Error; err != nil { + return + } + + samples = make([]face.Descriptor, len(imageFaces)) + faceGroupIDs = make([]int32, len(imageFaces)) + imageFaceIDs = make([]int, len(imageFaces)) + + for i, imgFace := range imageFaces { + samples[i] = face.Descriptor(imgFace.Descriptor) + faceGroupIDs[i] = int32(imgFace.FaceGroupID) + imageFaceIDs[i] = imgFace.ID + } + + return +} + +// ReloadFacesFromDatabase replaces the in-memory face descriptors with the ones in the database +func (fd *faceDetector) ReloadFacesFromDatabase(db *gorm.DB) error { + faceDescriptors, faceGroupIDs, imageFaceIDs, err := getSamplesFromDatabase(db) + if err != nil { + return err + } + + fd.mutex.Lock() + defer fd.mutex.Unlock() + + fd.faceDescriptors = faceDescriptors + fd.faceGroupIDs = faceGroupIDs + fd.imageFaceIDs = imageFaceIDs + + return nil +} + +// DetectFaces finds the faces in the given image and saves them to the database +func (fd *faceDetector) DetectFaces(db *gorm.DB, media *models.Media) error { + if err := db.Model(media).Preload("MediaURL").First(&media).Error; err != nil { + return err + } + + var thumbnailURL *models.MediaURL + for _, url := range media.MediaURL { + if url.Purpose == models.PhotoThumbnail { + thumbnailURL = &url + thumbnailURL.Media = media + break + } + } + + if thumbnailURL == nil { + return errors.New("thumbnail url is missing") + } + + thumbnailPath, err := thumbnailURL.CachedPath() + if err != nil { + return err + } + + fd.mutex.Lock() + faces, err := fd.rec.RecognizeFile(thumbnailPath) + fd.mutex.Unlock() + + if err != nil { + return errors.Wrap(err, "error read faces") + } + + for _, face := range faces { + fd.classifyFace(db, &face, media, thumbnailPath) + } + + return nil +} + +func (fd *faceDetector) classifyDescriptor(descriptor face.Descriptor) int32 { + return int32(fd.rec.ClassifyThreshold(descriptor, 0.2)) +} + +func (fd *faceDetector) classifyFace(db *gorm.DB, face *face.Face, media *models.Media, imagePath string) error { + fd.mutex.Lock() + defer fd.mutex.Unlock() + + match := fd.classifyDescriptor(face.Descriptor) + + dimension, err := media_encoding.GetPhotoDimensions(imagePath) + if err != nil { + return err + } + + imageFace := models.ImageFace{ + MediaID: media.ID, + Descriptor: models.FaceDescriptor(face.Descriptor), + Rectangle: models.FaceRectangle{ + // Converts a pixel absolute rectangle to a relative FaceRectangle. + MinX: float64(face.Rectangle.Min.X) / float64(dimension.Width), + MaxX: float64(face.Rectangle.Max.X) / float64(dimension.Width), + MinY: float64(face.Rectangle.Min.Y) / float64(dimension.Height), + MaxY: float64(face.Rectangle.Max.Y) / float64(dimension.Height), + }, + } + + var faceGroup models.FaceGroup + + // If no match add it new to samples + if match < 0 { + log.Println("No match, assigning new face") + + faceGroup = models.FaceGroup{ + ImageFaces: []models.ImageFace{imageFace}, + } + + if err := db.Create(&faceGroup).Error; err != nil { + return err + } + + } else { + log.Println("Found match") + + if err := db.First(&faceGroup, int(match)).Error; err != nil { + return err + } + + if err := db.Model(&faceGroup).Association("ImageFaces").Append(&imageFace); err != nil { + return err + } + } + + fd.faceDescriptors = append(fd.faceDescriptors, face.Descriptor) + fd.faceGroupIDs = append(fd.faceGroupIDs, int32(faceGroup.ID)) + fd.imageFaceIDs = append(fd.imageFaceIDs, imageFace.ID) + + fd.rec.SetSamples(fd.faceDescriptors, fd.faceGroupIDs) + return nil +} + +func (fd *faceDetector) MergeCategories(sourceID int32, destID int32) { + fd.mutex.Lock() + defer fd.mutex.Unlock() + + for i := range fd.faceGroupIDs { + if fd.faceGroupIDs[i] == sourceID { + fd.faceGroupIDs[i] = destID + } + } +} + +func (fd *faceDetector) MergeImageFaces(imageFaceIDs []int, destFaceGroupID int32) { + fd.mutex.Lock() + defer fd.mutex.Unlock() + + for i := range fd.faceGroupIDs { + imageFaceID := fd.imageFaceIDs[i] + + for _, id := range imageFaceIDs { + if imageFaceID == id { + fd.faceGroupIDs[i] = destFaceGroupID + break + } + } + } +} + +func (fd *faceDetector) RecognizeUnlabeledFaces(tx *gorm.DB, user *models.User) ([]*models.ImageFace, error) { + unrecognizedDescriptors := make([]face.Descriptor, 0) + unrecognizedFaceGroupIDs := make([]int32, 0) + unrecognizedImageFaceIDs := make([]int, 0) + + newFaceGroupIDs := make([]int32, 0) + newDescriptors := make([]face.Descriptor, 0) + newImageFaceIDs := make([]int, 0) + + var unlabeledFaceGroups []*models.FaceGroup + + err := tx. + Joins("JOIN image_faces ON image_faces.face_group_id = face_groups.id"). + Joins("JOIN media ON image_faces.media_id = media.id"). + Where("face_groups.label IS NULL"). + Where("media.album_id IN (?)", + tx.Select("album_id").Table("user_albums").Where("user_id = ?", user.ID), + ). + Find(&unlabeledFaceGroups).Error + + if err != nil { + return nil, err + } + + fd.mutex.Lock() + defer fd.mutex.Unlock() + + for i := range fd.faceDescriptors { + descriptor := fd.faceDescriptors[i] + faceGroupID := fd.faceGroupIDs[i] + imageFaceID := fd.imageFaceIDs[i] + + isUnlabeled := false + for _, unlabeledFaceGroup := range unlabeledFaceGroups { + if faceGroupID == int32(unlabeledFaceGroup.ID) { + isUnlabeled = true + continue + } + } + + if isUnlabeled { + unrecognizedFaceGroupIDs = append(unrecognizedFaceGroupIDs, faceGroupID) + unrecognizedDescriptors = append(unrecognizedDescriptors, descriptor) + unrecognizedImageFaceIDs = append(unrecognizedImageFaceIDs, imageFaceID) + } else { + newFaceGroupIDs = append(newFaceGroupIDs, faceGroupID) + newDescriptors = append(newDescriptors, descriptor) + newImageFaceIDs = append(newImageFaceIDs, imageFaceID) + } + } + + fd.faceGroupIDs = newFaceGroupIDs + fd.faceDescriptors = newDescriptors + fd.imageFaceIDs = newImageFaceIDs + + updatedImageFaces := make([]*models.ImageFace, 0) + + for i := range unrecognizedDescriptors { + descriptor := unrecognizedDescriptors[i] + faceGroupID := unrecognizedFaceGroupIDs[i] + imageFaceID := unrecognizedImageFaceIDs[i] + + match := fd.classifyDescriptor(descriptor) + + if match < 0 { + // still no match, we can readd it to the list + fd.faceGroupIDs = append(fd.faceGroupIDs, faceGroupID) + fd.faceDescriptors = append(fd.faceDescriptors, descriptor) + fd.imageFaceIDs = append(fd.imageFaceIDs, imageFaceID) + } else { + // found new match, update the database + var imageFace models.ImageFace + if err := tx.Model(&models.ImageFace{}).First(imageFace, imageFaceID).Error; err != nil { + return nil, err + } + + if err := tx.Model(&imageFace).Update("face_group_id", int(faceGroupID)).Error; err != nil { + return nil, err + } + + updatedImageFaces = append(updatedImageFaces, &imageFace) + + fd.faceGroupIDs = append(fd.faceGroupIDs, match) + fd.faceDescriptors = append(fd.faceDescriptors, descriptor) + fd.imageFaceIDs = append(fd.imageFaceIDs, imageFaceID) + } + } + + return updatedImageFaces, nil +} diff --git a/api/scanner/face_detection/face_detector_shim.go b/api/scanner/face_detection/face_detector_shim.go new file mode 100644 index 0000000..5490695 --- /dev/null +++ b/api/scanner/face_detection/face_detector_shim.go @@ -0,0 +1,14 @@ +//go:build no_face_detection + +package face_detection + +import ( + "log" + + "gorm.io/gorm" +) + +func InitializeFaceDetector(db *gorm.DB) error { + log.Printf("Face detection disabled (at build-time)") + return nil +} diff --git a/api/scanner/media_encoding/encode_photo.go b/api/scanner/media_encoding/encode_photo.go new file mode 100644 index 0000000..19f73e5 --- /dev/null +++ b/api/scanner/media_encoding/encode_photo.go @@ -0,0 +1,170 @@ +package media_encoding + +import ( + "context" + "fmt" + "image" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/utils" + "github.com/photoview/photoview/api/scanner/media_encoding/executable_worker" + "github.com/photoview/photoview/api/scanner/media_type" + "github.com/pkg/errors" + "gopkg.in/vansante/go-ffprobe.v2" + + "gorm.io/gorm" +) + +// Dimension presents the Dimension of a image. +type Dimension struct { + Width int + Height int +} + +// ThumbnailScale generates a new dimension for thumbnails. +func (d *Dimension) ThumbnailScale() Dimension { + if d.Height == 0 || d.Width == 0 { + return Dimension{Width: 0, Height: 0} + } + + aspect := float64(d.Width) / float64(d.Height) + + var width, height int + + if aspect > 1 { + width = 1024 + height = int(1024 / aspect) + } else { + width = int(1024 * aspect) + height = 1024 + } + + if width > d.Width { + width = d.Width + height = d.Height + } + + return Dimension{ + Width: width, + Height: height, + } +} + +// GetPhotoDimensions returns the dimension of the image `imagePath`. +func GetPhotoDimensions(imagePath string) (Dimension, error) { + w, h, err := executable_worker.Magick.IdentifyDimension(imagePath) + if err != nil { + return Dimension{}, fmt.Errorf("identify dimension %q error: %w", imagePath, err) + } + + return Dimension{ + Width: int(w), + Height: int(h), + }, nil +} + +// EncodeThumbnail encodes a thumbnail of `inputPath`, and store it as `outputPath`. +// It returns the dimension of the thumbnail. The thumbnail will be not bigger than 1024x1024. +func EncodeThumbnail(db *gorm.DB, inputPath string, outputPath string) (Dimension, error) { + w, h, err := executable_worker.Magick.IdentifyDimension(inputPath) + if err != nil { + return Dimension{}, fmt.Errorf("can't generate thumbnail of file %q: %w", inputPath, err) + } + + origin := Dimension{ + Width: int(w), + Height: int(h), + } + thumbnail := origin.ThumbnailScale() + + if err := executable_worker.Magick.GenerateThumbnail(inputPath, outputPath, uint(thumbnail.Width), uint(thumbnail.Height)); err != nil { + return Dimension{}, fmt.Errorf("can't generate thumbnail of file %q: %w", inputPath, err) + } + + w, h, err = executable_worker.Magick.IdentifyDimension(outputPath) + if err != nil { + return Dimension{}, fmt.Errorf("can't generate thumbnail of file %q: %w", inputPath, err) + } + thumbnail = Dimension{ + Width: int(w), + Height: int(h), + } + + return thumbnail, nil +} + +// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated +type EncodeMediaData struct { + Media *models.Media + CounterpartPath *string + _photoImage image.Image + _contentType media_type.MediaType + _videoMetadata *ffprobe.ProbeData +} + +func NewEncodeMediaData(media *models.Media) EncodeMediaData { + fileType := media_type.GetMediaType(media.Path) + + return EncodeMediaData{ + Media: media, + _contentType: fileType, + } +} + +// ContentType reads the image to determine its content type +func (img *EncodeMediaData) ContentType() (media_type.MediaType, error) { + if img._contentType != media_type.TypeUnknown { + return img._contentType, nil + } + + imgType := media_type.GetMediaType(img.Media.Path) + if imgType == media_type.TypeUnknown { + return imgType, fmt.Errorf("unknown type of %q", img.Media.Path) + } + + img._contentType = imgType + return imgType, nil +} + +func (img *EncodeMediaData) EncodeHighRes(outputPath string) error { + contentType, err := img.ContentType() + if err != nil { + return err + } + + if !contentType.IsSupported() { + return errors.New("could not convert photo as file format is not supported") + } + + // Use magick if there is no counterpart JPEG file to use instead + if contentType.IsImage() && !contentType.IsWebCompatible() { + imgPath := img.Media.Path + if img.CounterpartPath != nil { + imgPath = *img.CounterpartPath + } + + err := executable_worker.Magick.EncodeJpeg(imgPath, outputPath, 70) + if err != nil { + return fmt.Errorf("failed to convert RAW photo %q to JPEG: %w", imgPath, err) + } + } + + return nil +} + +func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.ProbeData, error) { + + if enc._videoMetadata != nil { + return enc._videoMetadata, nil + } + + ctx, cancelFn := context.WithTimeout(context.Background(), utils.MediaProbeTimeout()) + defer cancelFn() + data, err := ffprobe.ProbeURL(ctx, enc.Media.Path) + if err != nil { + return nil, errors.Wrapf(err, "could not read video metadata (%s)", enc.Media.Title) + } + + enc._videoMetadata = data + return enc._videoMetadata, nil +} diff --git a/api/scanner/media_encoding/executable_worker/executable_worker.go b/api/scanner/media_encoding/executable_worker/executable_worker.go new file mode 100644 index 0000000..e1241e4 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/executable_worker.go @@ -0,0 +1,53 @@ +package executable_worker + +import ( + "errors" + "fmt" + "os/exec" + "strings" + + "github.com/photoview/photoview/api/log" + "gopkg.in/vansante/go-ffprobe.v2" +) + +var ErrNoDependency = errors.New("dependency not found") +var ErrDisabledFunction = errors.New("function disabled") + +// Initialize Initializes all workers. It returns a function to terminate workers, which should be called before the program closing. +func Initialize() func() { + Magick = newMagickWand() + Ffmpeg = newFfmpegCli() + + if err := SetFfprobePath(); err != nil { + log.Error(nil, "Init ffprobe fail.", "error", err) + } + + return func() { + Magick.Terminate() + Magick = nil + } +} + +var Magick *MagickWand = nil +var Ffmpeg *FfmpegCli = nil + +type ExecutableWorker interface { + Path() string +} + +func SetFfprobePath() error { + path, err := exec.LookPath("ffprobe") + if err != nil { + return fmt.Errorf("Executable ffprobe not found: %w", err) + } + + version, err := exec.Command(path, "-version").Output() + if err != nil { + return fmt.Errorf("Executable ffprobe(%q) not executable: %w", path, err) + } + + log.Info(nil, "Found ffprobe", "path", path, "version", strings.Split(string(version), "\n")[0]) + ffprobe.SetFFProbeBinPath(path) + + return nil +} diff --git a/api/scanner/media_encoding/executable_worker/executable_worker_test.go b/api/scanner/media_encoding/executable_worker/executable_worker_test.go new file mode 100644 index 0000000..5a0a6d6 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/executable_worker_test.go @@ -0,0 +1,59 @@ +package executable_worker + +import ( + "path/filepath" + "runtime" + "strings" + "testing" + + _ "github.com/photoview/photoview/api/test_utils/flags" +) + +const testdataBinPath = "./test_data/mock_bin" + +// SetPathWithCurrent sets PATH env to `paths` in the directory of testing files. The PATH will restore to the previous value when the test is done. +func SetPathWithCurrent(t *testing.T, paths ...string) { + _, file, _, ok := runtime.Caller(1) + if !ok { + t.Log("Can't get the test file. Ignore setting PATH.") + return + } + + base := filepath.Dir(file) + + for i, path := range paths { + paths[i] = filepath.Join(base, path) + } + + t.Setenv("PATH", strings.Join(paths, ":")) +} + +func TestInitFfprobePath(t *testing.T) { + t.Run("PathFail", func(t *testing.T) { + SetPathWithCurrent(t, "non_exist_path") + + err := SetFfprobePath() + if err == nil { + t.Fatalf("InitFfprobePath() returns nil, want an error") + } + }) + + t.Run("VersionFail", func(t *testing.T) { + SetPathWithCurrent(t, testdataBinPath) + t.Setenv("FAIL_WITH", "expect failure") + + err := SetFfprobePath() + if err == nil { + t.Fatalf("InitFfprobePath() returns nil, want an error") + } + }) + + t.Run("Succeed", func(t *testing.T) { + SetPathWithCurrent(t, testdataBinPath) + + err := SetFfprobePath() + if err != nil { + t.Fatalf("InitFfprobePath() returns %v, want nil", err) + } + }) +} diff --git a/api/scanner/media_encoding/executable_worker/ffmpeg_cli.go b/api/scanner/media_encoding/executable_worker/ffmpeg_cli.go new file mode 100644 index 0000000..8ad3c77 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/ffmpeg_cli.go @@ -0,0 +1,122 @@ +package executable_worker + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/utils" + "gopkg.in/vansante/go-ffprobe.v2" +) + +const defaultCodec = "h264" + +var hwAccToCodec = map[string]string{ + "qsv": defaultCodec + "_qsv", + "vaapi": defaultCodec + "_vaapi", + "nvenc": defaultCodec + "_nvenc", +} + +type FfmpegCli struct { + path string + videoCodec string + err error +} + +func newFfmpegCli() *FfmpegCli { + if utils.EnvDisableVideoEncoding.GetBool() { + log.Warn(nil, "Executable ffmpeg worker disabled", utils.EnvDisableVideoEncoding.GetName(), utils.EnvDisableVideoEncoding.GetValue()) + return &FfmpegCli{ + err: ErrDisabledFunction, + } + } + + path, err := exec.LookPath("ffmpeg") + if err != nil { + log.Error(nil, "Executable ffmpeg worker not found") + return &FfmpegCli{ + err: ErrNoDependency, + } + } + + version, err := exec.Command(path, "-version").Output() + if err != nil { + log.Error(nil, "Executable ffmpeg worker getting version error", "error", err) + return &FfmpegCli{ + err: ErrNoDependency, + } + } + + hwAcc := utils.EnvVideoHardwareAcceleration.GetValue() + codec, ok := hwAccToCodec[hwAcc] + if !ok { + if strings.HasPrefix(hwAcc, "_") { + // A secret way to set the codec directly. + codec = hwAcc[1:] + } else { + codec = defaultCodec + } + } + + log.Info(nil, "Found executable worker: ffmpeg", "version", strings.Split(string(version), "\n")[0], "codec", codec) + + return &FfmpegCli{ + path: path, + videoCodec: codec, + } +} + +func (cli *FfmpegCli) IsInstalled() bool { + return cli.err == nil +} + +func (cli *FfmpegCli) EncodeMp4(inputPath string, outputPath string) error { + if cli.err != nil { + return fmt.Errorf("encoding video %q error: ffmpeg: %w", inputPath, cli.err) + } + + args := []string{ + "-i", + inputPath, + "-vcodec", cli.videoCodec, + "-acodec", "aac", + "-vf", "scale='min(1080,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2", + "-movflags", "+faststart+use_metadata_tags", + outputPath, + } + + cmd := exec.Command(cli.path, args...) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("encoding video with %q %v error: %w", cli.path, args, err) + } + + return nil +} + +func (cli *FfmpegCli) EncodeVideoThumbnail(inputPath string, outputPath string, probeData *ffprobe.ProbeData) error { + if cli.err != nil { + return fmt.Errorf("encoding video thumbnail %q error: ffmpeg: %w", inputPath, cli.err) + } + + thumbnailOffsetSeconds := fmt.Sprintf("%.f", probeData.Format.DurationSeconds*0.25) + + args := []string{ + "-ss", thumbnailOffsetSeconds, // grab frame at time offset + "-i", + inputPath, + "-vframes", "1", // output one frame + "-an", // disable audio + "-vf", "scale='min(1024,iw)':'min(1024,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2", + outputPath, + } + + cmd := exec.Command(cli.path, args...) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("encoding video thumbnail with %q %v error: %w", cli.path, args, err) + } + + return nil +} diff --git a/api/scanner/media_encoding/executable_worker/ffmpeg_cli_test.go b/api/scanner/media_encoding/executable_worker/ffmpeg_cli_test.go new file mode 100644 index 0000000..df1f453 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/ffmpeg_cli_test.go @@ -0,0 +1,165 @@ +package executable_worker + +import ( + "errors" + "regexp" + "testing" + + "github.com/photoview/photoview/api/utils" + "gopkg.in/vansante/go-ffprobe.v2" +) + +func TestFfmpegNotExist(t *testing.T) { + SetPathWithCurrent(t, "") + + Ffmpeg = newFfmpegCli() + + if got, want := Ffmpeg.err, ErrNoDependency; got != want { + t.Errorf("Ffmpeg.err = %v, want: %v", got, want) + } + + if Ffmpeg.IsInstalled() { + t.Error("Ffmpeg should not be installed, but is found:", Ffmpeg) + } + + if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrNoDependency; !errors.Is(got, want) { + t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want) + } + + if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrNoDependency; !errors.Is(got, want) { + t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want) + } +} + +func TestFfmpegVersionFail(t *testing.T) { + SetPathWithCurrent(t, testdataBinPath) + t.Setenv("FAIL_WITH", "expect failure") + + Ffmpeg = newFfmpegCli() + + if got, want := Ffmpeg.err, ErrNoDependency; got != want { + t.Errorf("Ffmpeg.err = %v, want: %v", got, want) + } + + if Ffmpeg.IsInstalled() { + t.Error("Ffmpeg should not be installed, but is found:", Ffmpeg) + } + + if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrNoDependency; !errors.Is(got, want) { + t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want) + } + + if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrNoDependency; !errors.Is(got, want) { + t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want) + } +} + +func TestFfmpegIgnore(t *testing.T) { + SetPathWithCurrent(t, testdataBinPath) + t.Setenv("PHOTOVIEW_DISABLE_VIDEO_ENCODING", "true") + + Ffmpeg = newFfmpegCli() + + if got, want := Ffmpeg.err, ErrDisabledFunction; got != want { + t.Errorf("Ffmpeg.err = %v, want: %v", got, want) + } + + if Ffmpeg.IsInstalled() { + t.Error("Ffmpeg should be ignored (as it is disabled), but is initialized:", Ffmpeg) + } + + if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrDisabledFunction; !errors.Is(got, want) { + t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want) + } + + if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrDisabledFunction; !errors.Is(got, want) { + t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want) + } +} + +func TestFfmpeg(t *testing.T) { + SetPathWithCurrent(t, testdataBinPath) + + Ffmpeg = newFfmpegCli() + + if !Ffmpeg.IsInstalled() { + t.Fatal("Ffmpeg should be installed") + } + + t.Run("EncodeMp4Failed", func(t *testing.T) { + t.Setenv("FAIL_WITH", "expect failure") + + err := Ffmpeg.EncodeMp4("input", "output") + if err == nil { + t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.") + } + if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec h264 .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) { + t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want) + } + }) + + t.Run("EncodeMp4Succeeded", func(t *testing.T) { + err := Ffmpeg.EncodeMp4("input", "output") + if err != nil { + t.Fatalf("Ffmpeg.EncodeMp4(...) = %v, should be nil.", err) + } + }) + + probeData := &ffprobe.ProbeData{ + Format: &ffprobe.Format{ + DurationSeconds: 10, + }, + } + t.Run("EncodeVideoThumbnailMp4Failed", func(t *testing.T) { + t.Setenv("FAIL_WITH", "expect failure") + + err := Ffmpeg.EncodeVideoThumbnail("input", "output", probeData) + if err == nil { + t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = nil, should be an error.") + } + if got, want := err.Error(), `^encoding video thumbnail with ".*/test_data/mock_bin/ffmpeg" \[-ss 2 -i input .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) { + t.Errorf("Ffmpeg.EncodeVideoThumbnail(...) = %q, should be as reg pattern %q", got, want) + } + }) + + t.Run("EncodeVideoThumbnailSucceeded", func(t *testing.T) { + err := Ffmpeg.EncodeVideoThumbnail("input", "output", probeData) + if err != nil { + t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = %v, should be nil.", err) + } + }) +} + +func TestFfmpegWithHWAcc(t *testing.T) { + SetPathWithCurrent(t, testdataBinPath) + t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "qsv") + + Ffmpeg = newFfmpegCli() + + t.Setenv("FAIL_WITH", "expect failure") + + err := Ffmpeg.EncodeMp4("input", "output") + if err == nil { + t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.") + } + if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec h264_qsv .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) { + t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want) + } +} + +func TestFfmpegWithCustomCodec(t *testing.T) { + SetPathWithCurrent(t, testdataBinPath) + t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "_custom") + + Ffmpeg = newFfmpegCli() + + t.Setenv("FAIL_WITH", "expect failure") + + err := Ffmpeg.EncodeMp4("input", "output") + if err == nil { + t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.") + } + if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec custom .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) { + t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want) + } +} diff --git a/api/scanner/media_encoding/executable_worker/magickwand.go b/api/scanner/media_encoding/executable_worker/magickwand.go new file mode 100644 index 0000000..18cfa94 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/magickwand.go @@ -0,0 +1,118 @@ +package executable_worker + +import ( + "fmt" + + "github.com/photoview/photoview/api/log" + "gopkg.in/gographics/imagick.v3/imagick" +) + +type MagickWand struct { + initialized bool +} + +func newMagickWand() *MagickWand { + imagick.Initialize() + + verstr, vernum := imagick.GetVersion() + + log.Info(nil, "Found magickwand worker: "+verstr, "version", vernum) + + return &MagickWand{ + initialized: true, + } +} + +func (cli *MagickWand) Terminate() { + cli.initialized = false + imagick.Terminate() +} + +func (cli *MagickWand) IsInstalled() bool { + return cli != nil && cli.initialized +} + +func (cli *MagickWand) EncodeJpeg(inputPath string, outputPath string, jpegQuality uint) error { + wand, err := cli.createWandFromFile(inputPath) + if err != nil { + return err + } + defer wand.Destroy() + + if err := wand.SetFormat("JPEG"); err != nil { + return fmt.Errorf("ImagickWand set JPEG format for %q error: %w", inputPath, err) + } + + if err := wand.SetImageCompressionQuality(jpegQuality); err != nil { + return fmt.Errorf("ImagickWand set JPEG quality %d for %q error: %w", jpegQuality, inputPath, err) + } + + if err := wand.WriteImage(outputPath); err != nil { + return fmt.Errorf("ImagickWand write %q error: %w", outputPath, err) + } + + return nil +} + +func (cli *MagickWand) GenerateThumbnail(inputPath string, outputPath string, width, height uint) error { + wand, err := cli.createWandFromFile(inputPath) + if err != nil { + return err + } + defer wand.Destroy() + + if err := wand.ThumbnailImage(width, height); err != nil { + return fmt.Errorf("ImagickWand generate thumbnail for %q error: %w", inputPath, err) + } + + if err := wand.SetFormat("JPEG"); err != nil { + return fmt.Errorf("ImagickWand set JPEG format for %q error: %w", inputPath, err) + } + + if err := wand.SetImageCompressionQuality(70); err != nil { + return fmt.Errorf("ImagickWand set JPEG quality %d for %q error: %w", 70, inputPath, err) + } + + if err := wand.WriteImage(outputPath); err != nil { + return fmt.Errorf("ImagickWand write %q error: %w", outputPath, err) + } + + return nil +} + +func (cli *MagickWand) IdentifyDimension(inputPath string) (width, height uint, reterr error) { + wand, err := cli.createWandFromFile(inputPath) + if err != nil { + reterr = err + return + } + defer wand.Destroy() + + width = wand.GetImageWidth() + height = wand.GetImageHeight() + + return +} + +func (cli *MagickWand) createWandFromFile(inputPath string) (*imagick.MagickWand, error) { + if !cli.IsInstalled() { + return nil, fmt.Errorf("ImagickWand is not initialized") + } + + wand := imagick.NewMagickWand() + + if err := wand.ReadImage(inputPath); err != nil { + return nil, fmt.Errorf("ImagickWand read %q error: %w", inputPath, err) + } + + if err := wand.AutoOrientImage(); err != nil { + return nil, fmt.Errorf("ImagickWand auto-orient %q error: %w", inputPath, err) + } + + // Reset EXIF orientation to 1 (top-left) since image is now properly oriented + if err := wand.SetImageOrientation(imagick.ORIENTATION_TOP_LEFT); err != nil { + return nil, fmt.Errorf("ImagickWand set orientation for %q error: %w", inputPath, err) + } + + return wand, nil +} diff --git a/api/scanner/media_encoding/executable_worker/test_data/mock_bin/ffmpeg b/api/scanner/media_encoding/executable_worker/test_data/mock_bin/ffmpeg new file mode 100644 index 0000000..c98e478 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/test_data/mock_bin/ffmpeg @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +: ${FAIL_WITH=""} + +case "$1" in + "--version") + echo ffmpeg: version fake + ;; +esac + +if [ "${FAIL_WITH}" != "" ] +then + echo ${FAIL_WITH} + exit -1 +fi + +echo $@ diff --git a/api/scanner/media_encoding/executable_worker/test_data/mock_bin/ffprobe b/api/scanner/media_encoding/executable_worker/test_data/mock_bin/ffprobe new file mode 100644 index 0000000..3a044d2 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/test_data/mock_bin/ffprobe @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +: ${FAIL_WITH=""} + +case "$1" in + "--version") + echo ffprobe: version fake + ;; +esac + +if [ "${FAIL_WITH}" != "" ] +then + echo ${FAIL_WITH} + exit -1 +fi + +echo $@ diff --git a/api/scanner/media_encoding/executable_worker/test_data/mock_bin/magick b/api/scanner/media_encoding/executable_worker/test_data/mock_bin/magick new file mode 100644 index 0000000..8e5daf3 --- /dev/null +++ b/api/scanner/media_encoding/executable_worker/test_data/mock_bin/magick @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu + +: ${FAIL_WITH=""} +: ${INVALID_OUTPUT=""} + +if [ "${FAIL_WITH}" != "" ] +then + echo ${FAIL_WITH} + exit -1 +fi + +if [ "$#" = "0" ] # no args +then + exit 0 +fi + +case "$1" in + "-version") + echo magick: version fake + exit 0 + ;; + "identify") + if [ "${INVALID_OUTPUT}" != "" ] + then + echo -n ${INVALID_OUTPUT} + exit 0 + fi + + echo -n '{"height":800, "width":1000}' + exit 0 + ;; +esac + +echo $@ diff --git a/api/scanner/media_scan.go b/api/scanner/media_scan.go new file mode 100644 index 0000000..2fccb62 --- /dev/null +++ b/api/scanner/media_scan.go @@ -0,0 +1,40 @@ +package scanner + +import ( + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_tasks" + "github.com/pkg/errors" +) + +func scanMedia(ctx scanner_task.TaskContext, media *models.Media, mediaData *media_encoding.EncodeMediaData, mediaIndex int, mediaTotal int) error { + newCtx, err := scanner_tasks.Tasks.BeforeProcessMedia(ctx, mediaData) + if err != nil { + return errors.Wrapf(err, "before process media (%s)", media.Path) + } + + mediaCachePath, err := media.CachePath() + if err != nil { + return errors.Wrapf(err, "cache directory error (%s)", media.Path) + } + + transactionError := newCtx.DatabaseTransaction(func(ctx scanner_task.TaskContext) error { + updatedURLs, err := scanner_tasks.Tasks.ProcessMedia(newCtx, mediaData, mediaCachePath) + if err != nil { + return errors.Wrapf(err, "process media (%s)", media.Path) + } + + if err = scanner_tasks.Tasks.AfterProcessMedia(newCtx, mediaData, updatedURLs, mediaIndex, mediaTotal); err != nil { + return errors.Wrap(err, "after process media") + } + + return nil + }) + + if transactionError != nil { + return errors.Wrap(transactionError, "process media database transaction") + } + + return nil +} diff --git a/api/scanner/media_type/counterpart.go b/api/scanner/media_type/counterpart.go new file mode 100644 index 0000000..e8b1cff --- /dev/null +++ b/api/scanner/media_type/counterpart.go @@ -0,0 +1,45 @@ +package media_type + +import ( + "path" + "path/filepath" + "strings" +) + +// FindWebCounterpart returns the filename if the file `imagePath` has a counterpart file competible with the browser. +func FindWebCounterpart(imagePath string) (string, bool) { + return findCounterpart(imagePath, func(filename string) bool { + mt := GetMediaType(filename) + return mt.IsImage() && mt.IsWebCompatible() + }) +} + +// FindRawCounterpart returns the filename if the file `imagePath` has a counterpart file which needs to be processed before showing in the browser. +func FindRawCounterpart(imagePath string) (string, bool) { + return findCounterpart(imagePath, func(filename string) bool { + mt := GetMediaType(filename) + return mt.IsImage() && !mt.IsWebCompatible() + }) +} + +func findCounterpart(filename string, acceptFn func(filepath string) bool) (string, bool) { + ext := path.Ext(filename) + filenamePattern := strings.TrimSuffix(filename, ext) + ".*" + + filenames, err := filepath.Glob(filenamePattern) + if err != nil { + return "", false + } + + for _, f := range filenames { + if f == filename { + continue + } + + if acceptFn(f) { + return f, true + } + } + + return "", false +} diff --git a/api/scanner/media_type/counterpart_test.go b/api/scanner/media_type/counterpart_test.go new file mode 100644 index 0000000..53223fa --- /dev/null +++ b/api/scanner/media_type/counterpart_test.go @@ -0,0 +1,66 @@ +package media_type + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestFindWebCounterpart(t *testing.T) { + mediaPath := test_utils.PathFromAPIRoot("scanner", "test_media", "real_media") + + tests := []struct { + input string + wantFile string + wantOk bool + }{ + {"raw_with_jpg.tiff", "raw_with_jpg.jpg", true}, + {"raw_with_file.tiff", "", false}, + {"standalone_raw.tiff", "", false}, + } + + for _, tc := range tests { + input := filepath.Join(mediaPath, tc.input) + if _, err := os.Stat(input); err != nil { + t.Fatalf("input %q doesn't exist: %v", input, err) + } + + file, ok := FindWebCounterpart(input) + got := strings.TrimLeft(strings.TrimPrefix(file, mediaPath), "/") + + if got != tc.wantFile || ok != tc.wantOk { + t.Errorf("FindWebCounterpart(%q) = (%q, %v), want: (%q, %v)", tc.input, got, ok, tc.wantFile, tc.wantOk) + } + } +} + +func TestFindRawCounterpart(t *testing.T) { + mediaPath := test_utils.PathFromAPIRoot("scanner", "test_media", "real_media") + + tests := []struct { + input string + wantFile string + wantOk bool + }{ + {"raw_with_jpg.jpg", "raw_with_jpg.tiff", true}, + {"jpg_with_file.jpg", "", false}, + {"standalone_jpg.jpg", "", false}, + } + + for _, tc := range tests { + input := filepath.Join(mediaPath, tc.input) + if _, err := os.Stat(input); err != nil { + t.Fatalf("input %q doesn't exist: %v", input, err) + } + + file, ok := FindRawCounterpart(input) + got := strings.TrimLeft(strings.TrimPrefix(file, mediaPath), "/") + + if got != tc.wantFile || ok != tc.wantOk { + t.Errorf("FindRawCounterpart(%q) = (%q, %v), want: (%q, %v)", tc.input, got, ok, tc.wantFile, tc.wantOk) + } + } +} diff --git a/api/scanner/media_type/media_type.go b/api/scanner/media_type/media_type.go new file mode 100644 index 0000000..8342444 --- /dev/null +++ b/api/scanner/media_type/media_type.go @@ -0,0 +1,136 @@ +package media_type + +import ( + "strings" + "unique" + + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/externaltools/exif" +) + +type MediaType unique.Handle[string] + +// GetMediaType returns a media type of file `f`. +// This function is thread-safe. +func GetMediaType(f string) MediaType { + mime, err := exif.MIMEType(f) + if err != nil { + log.Warn(nil, "GetMediaType() error.", "error", err, "file", f) + return TypeUnknown + } + + return mediaType(mime) +} + +func mediaType(mime string) MediaType { + return MediaType(unique.Make(mime)) +} + +var ( + TypeUnknown MediaType + + TypeImage = mediaType("image/") + TypeVideo = mediaType("video/") + + // Web Image formats + TypeJPEG = mediaType("image/jpeg") + TypePNG = mediaType("image/png") + TypeWebP = mediaType("image/webp") + TypeBMP = mediaType("image/bmp") + TypeGIF = mediaType("image/gif") + + // Web Video formats + TypeMP4 = mediaType("video/mp4") + TypeMPEG = mediaType("video/mpeg") + TypeOGG = mediaType("video/ogg") + TypeWEBM = mediaType("video/webm") +) + +var webImageMimetypes = arrayToSet([]MediaType{ + TypeJPEG, + TypePNG, + TypeWebP, + TypeBMP, + TypeGIF, +}) + +var webVideoMimetypes = arrayToSet([]MediaType{ + TypeMP4, + TypeMPEG, + TypeWEBM, + TypeOGG, +}) + +// Legacy function. Should be removed. +var WebMimetypes = []string{ + TypeJPEG.String(), + TypePNG.String(), + TypeWebP.String(), + TypeBMP.String(), + TypeGIF.String(), + + TypeMP4.String(), + TypeMPEG.String(), + TypeWEBM.String(), + TypeOGG.String(), +} + +func arrayToSet[T comparable](array []T) map[T]struct{} { + ret := make(map[T]struct{}) + for _, item := range array { + ret[item] = struct{}{} + } + return ret +} + +// IsWebCompatible returns true if the media type is compatible with the browser. +func (t MediaType) IsWebCompatible() bool { + if t == TypeUnknown { + return false + } + + if _, ok := webImageMimetypes[t]; ok { + return true + } + + if _, ok := webVideoMimetypes[t]; ok { + return true + } + + return false +} + +// IsImage returns true if the media type is image type. +func (t MediaType) IsImage() bool { + if t == TypeUnknown { + return false + } + + return strings.HasPrefix(t.String(), TypeImage.String()) +} + +// IsVideo returns true if the media type is video type. +func (t MediaType) IsVideo() bool { + if t == TypeUnknown { + return false + } + + return strings.HasPrefix(t.String(), TypeVideo.String()) +} + +// IsSupported returns true if the media type can be processed. +func (t MediaType) IsSupported() bool { + if t == TypeUnknown { + return false + } + + return t.IsImage() || t.IsVideo() +} + +func (t MediaType) String() string { + if t == TypeUnknown { + return "unknown" + } + + return unique.Handle[string](t).Value() +} diff --git a/api/scanner/media_type/media_type_test.go b/api/scanner/media_type/media_type_test.go new file mode 100644 index 0000000..2ba558c --- /dev/null +++ b/api/scanner/media_type/media_type_test.go @@ -0,0 +1,144 @@ +package media_type + +import ( + "path/filepath" + "sync" + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} + +func TestGetMediaType(t *testing.T) { + mediaPath := test_utils.PathFromAPIRoot("scanner", "test_media", "real_media") + + tests := []struct { + filepath string + filetype MediaType + }{ + {"file.pdf", mediaType("text/plain")}, + + {"bmp.bmp", TypeBMP}, + {"gif.gif", TypeGIF}, + {"jpeg.jpg", TypeJPEG}, + {"png.png", TypePNG}, + {"webp.webp", TypeWebP}, + + {"heif.heif", mediaType("image/heic")}, + {"jpg2000.jp2", mediaType("image/jp2")}, + {"tiff.tiff", mediaType("image/tiff")}, + {"cr3.cr3", mediaType("image/x-canon-cr3")}, + + {"mp4.mp4", TypeMP4}, + {"ogg.ogg", TypeOGG}, + {"mpeg.mpg", TypeMPEG}, + {"webm.webm", TypeWEBM}, + + {"avi.avi", mediaType("video/x-msvideo")}, + {"mkv.mkv", mediaType("video/x-matroska")}, + {"quicktime.mov", mediaType("video/quicktime")}, + {"wmv.wmv", mediaType("video/x-ms-wmv")}, + } + + var wg sync.WaitGroup + defer wg.Wait() + + for _, tc := range tests { + wg.Add(1) + input, want := tc.filepath, tc.filetype + + go func() { + defer wg.Done() + + path := filepath.Join(mediaPath, input) + + got := GetMediaType(path) + if got != want { + t.Errorf("magic.Type(%q) = %v, want: %v", path, got, want) + } + }() + } +} + +type boolImage bool + +const isImage boolImage = true + +type boolVideo bool + +const isVideo boolVideo = true + +type boolWebCompatible bool + +const isWebCompatible boolWebCompatible = true + +type boolSupport bool + +const isSupport boolSupport = true + +func TestMediaTypeNoDeps(t *testing.T) { + tests := []struct { + mtype MediaType + wantIsImage boolImage + wantIsVideo boolVideo + wantIsWebCompatible boolWebCompatible + wantIsSupport boolSupport + }{ + // Unknown and unsupported types + {TypeUnknown, !isImage, !isVideo, !isWebCompatible, !isSupport}, + {mediaType("application/pdf"), !isImage, !isVideo, !isWebCompatible, !isSupport}, + + // Raw media types + {mediaType("image/some-raw-type"), isImage, !isVideo, !isWebCompatible, isSupport}, + {mediaType("video/some-video-type"), !isImage, isVideo, !isWebCompatible, isSupport}, + + // Generic types + {TypeImage, isImage, !isVideo, !isWebCompatible, isSupport}, + {TypeVideo, !isImage, isVideo, !isWebCompatible, isSupport}, + + // Web-compatible image types + {TypeJPEG, isImage, !isVideo, isWebCompatible, isSupport}, + {TypePNG, isImage, !isVideo, isWebCompatible, isSupport}, + {TypeWebP, isImage, !isVideo, isWebCompatible, isSupport}, + {TypeBMP, isImage, !isVideo, isWebCompatible, isSupport}, + {TypeGIF, isImage, !isVideo, isWebCompatible, isSupport}, + + // Web-compatible video types + {TypeMP4, !isImage, isVideo, isWebCompatible, isSupport}, + {TypeMPEG, !isImage, isVideo, isWebCompatible, isSupport}, + {TypeOGG, !isImage, isVideo, isWebCompatible, isSupport}, + {TypeWEBM, !isImage, isVideo, isWebCompatible, isSupport}, + } + + for _, tc := range tests { + gotImage := tc.mtype.IsImage() + if got, want := gotImage, bool(tc.wantIsImage); got != want { + t.Errorf("MediaType(%q).IsImage() = %v, want: %v", tc.mtype, got, want) + } + + gotVideo := tc.mtype.IsVideo() + if got, want := gotVideo, bool(tc.wantIsVideo); got != want { + t.Errorf("MediaType(%q).IsVideo() = %v, want: %v", tc.mtype, got, want) + } + + gotWebCompatible := tc.mtype.IsWebCompatible() + if got, want := gotWebCompatible, bool(tc.wantIsWebCompatible); got != want { + t.Errorf("MediaType(%q).IsWebCompatible() = %v, want: %v", tc.mtype, got, want) + } + + gotSupport := tc.mtype.IsSupported() + if got, want := gotSupport, bool(tc.wantIsSupport); got != want { + t.Errorf("MediaType(%q).IsSupported() = %v, want: %v", tc.mtype, got, want) + } + } +} + +func TestMediaTypeUnknown(t *testing.T) { + var got MediaType + if want := TypeUnknown; got != want { + t.Errorf("MediaType zero value should be TypeUnknown, which is not") + } +} diff --git a/api/scanner/perf_test.go b/api/scanner/perf_test.go new file mode 100644 index 0000000..0c95098 --- /dev/null +++ b/api/scanner/perf_test.go @@ -0,0 +1,77 @@ +package scanner + +import ( + "image" + "image/jpeg" + _ "image/png" + "os" + "path/filepath" + "testing" + + "gopkg.in/gographics/imagick.v3/imagick" +) + +func BenchmarkStdlib(b *testing.B) { + dir := b.TempDir() + + for b.Loop() { + func() { + input, err := os.Open("./test_media/real_media/png.png") + if err != nil { + b.Fatal("open error:", err) + } + defer input.Close() + + img, _, err := image.Decode(input) + if err != nil { + b.Fatal("decode error:", err) + } + + outfile := filepath.Join(dir, "test.jpg") + defer os.Remove(outfile) + + output, err := os.Create(outfile) + if err != nil { + b.Fatal("create error:", err) + } + defer output.Close() + + if err := jpeg.Encode(output, img, &jpeg.Options{Quality: 70}); err != nil { + b.Fatal("encode error:", err) + } + }() + } +} + +func BenchmarkMagickWand(b *testing.B) { + dir := b.TempDir() + + imagick.Initialize() + defer imagick.Terminate() + + for b.Loop() { + func() { + mw := imagick.NewMagickWand() + defer mw.Destroy() + + if err := mw.ReadImage("./test_media/real_media/png.png"); err != nil { + b.Fatal("read error:", err) + } + + output := filepath.Join(dir, "test.jpg") + defer os.Remove(output) + + if err := mw.SetFormat("JPEG"); err != nil { + b.Fatal("set format error:", err) + } + + if err := mw.SetImageCompressionQuality(70); err != nil { + b.Fatal("set quality error:", err) + } + + if err := mw.WriteImage(output); err != nil { + b.Fatal("write error:", err) + } + }() + } +} diff --git a/api/scanner/periodic_scanner/periodic_scanner.go b/api/scanner/periodic_scanner/periodic_scanner.go new file mode 100644 index 0000000..ae7c80d --- /dev/null +++ b/api/scanner/periodic_scanner/periodic_scanner.go @@ -0,0 +1,175 @@ +package periodic_scanner + +import ( + "fmt" + "sync" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/scanner_queue" + "gorm.io/gorm" +) + +type ScannerQueue interface { + AddAllToQueue() error +} + +type RealScannerQueue struct{} + +func (r *RealScannerQueue) AddAllToQueue() error { + return scanner_queue.AddAllToQueue() +} + +type periodicScanner struct { + ticker *time.Ticker + tickerLocker sync.Mutex + ticker_changed chan bool + done chan struct{} + db *gorm.DB + scannerQueue ScannerQueue +} + +var mainPeriodicScanner *periodicScanner = nil +var mainPeriodicScannerLocker sync.Mutex + +func getPeriodicScanInterval(db *gorm.DB) (time.Duration, error) { + var siteInfo models.SiteInfo + if err := db.First(&siteInfo).Error; err != nil { + return 0, err + } + + return time.Duration(siteInfo.PeriodicScanInterval) * time.Second, nil +} + +func InitializePeriodicScannerWithQueue(db *gorm.DB, queue ScannerQueue) error { + mainPeriodicScannerLocker.Lock() + defer mainPeriodicScannerLocker.Unlock() + + if mainPeriodicScanner != nil { + return fmt.Errorf("periodic scanner has already been initialized") + } + + scanInterval, err := getPeriodicScanInterval(db) + if err != nil { + return err + } + + mainPeriodicScanner = &periodicScanner{ + db: db, + ticker_changed: make(chan bool), + done: make(chan struct{}), + tickerLocker: sync.Mutex{}, + scannerQueue: queue, + } + + go mainPeriodicScanner.scanIntervalRunner() + + var newTicker *time.Ticker = nil + if scanInterval > 0 { + newTicker = time.NewTicker(scanInterval) + log.Info(nil, "Periodic scan interval changed: "+scanInterval.String()) + } else { + log.Info(nil, "Periodic scan interval changed: disabled") + } + + mainPeriodicScanner.ticker = newTicker + + select { + case mainPeriodicScanner.ticker_changed <- true: + default: + // Channel might be full, but that's okay + } + + return nil +} + +func InitializePeriodicScanner(db *gorm.DB) error { + return InitializePeriodicScannerWithQueue(db, &RealScannerQueue{}) +} + +func ChangePeriodicScanInterval(duration time.Duration) { + var newTicker *time.Ticker = nil + if duration > 0 { + newTicker = time.NewTicker(duration) + log.Info(nil, "Periodic scan interval changed: "+duration.String()) + } else { + log.Info(nil, "Periodic scan interval changed: disabled") + } + + mainPeriodicScannerLocker.Lock() + scanner := mainPeriodicScanner + mainPeriodicScannerLocker.Unlock() + if scanner != nil { + scanner.tickerLocker.Lock() + defer scanner.tickerLocker.Unlock() + + if scanner.ticker != nil { + scanner.ticker.Stop() + } + + scanner.ticker = newTicker + select { + case scanner.ticker_changed <- true: + default: + // Channel might be full, but that's okay + } + } +} + +// ShutdownPeriodicScanner gracefully shuts down the periodic scanner +func ShutdownPeriodicScanner() { + mainPeriodicScannerLocker.Lock() + defer mainPeriodicScannerLocker.Unlock() + + if mainPeriodicScanner != nil { + log.Info(nil, "Shutting down periodic scanner") + + // Signal the runner goroutine to stop + close(mainPeriodicScanner.done) + + // Stop the ticker if it exists + mainPeriodicScanner.tickerLocker.Lock() + if mainPeriodicScanner.ticker != nil { + mainPeriodicScanner.ticker.Stop() + mainPeriodicScanner.ticker = nil + } + mainPeriodicScanner.tickerLocker.Unlock() + + // Reset the global scanner + mainPeriodicScanner = nil + } +} + +func (ps *periodicScanner) scanIntervalRunner() { + for { + log.Info(nil, "Scan interval runner: Waiting for signal") + + ps.tickerLocker.Lock() + ticker := ps.ticker + ps.tickerLocker.Unlock() + + if ticker != nil { + select { + case <-ps.done: + log.Info(nil, "Scan interval runner: Shutting down") + return + case <-ps.ticker_changed: + log.Info(nil, "Scan interval runner: New ticker detected") + case <-ticker.C: + log.Info(nil, "Scan interval runner: Starting periodic scan") + if err := ps.scannerQueue.AddAllToQueue(); err != nil { + log.Error(nil, "Scan interval runner: Failed to add all users to queue", "error", err) + } + } + } else { + select { + case <-ps.done: + log.Info(nil, "Scan interval runner: Shutting down") + return + case <-ps.ticker_changed: + log.Info(nil, "Scan interval runner: New ticker detected") + } + } + } +} diff --git a/api/scanner/periodic_scanner/periodic_scanner_test.go b/api/scanner/periodic_scanner/periodic_scanner_test.go new file mode 100644 index 0000000..2dd48a4 --- /dev/null +++ b/api/scanner/periodic_scanner/periodic_scanner_test.go @@ -0,0 +1,279 @@ +package periodic_scanner + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "gorm.io/gorm" +) + +// MockScannerQueue implements the ScannerQueue interface for testing +type MockScannerQueue struct { + mock.Mock +} + +func (m *MockScannerQueue) AddAllToQueue() error { + return m.Called().Error(0) +} + +func TestMain(m *testing.M) { + test_utils.UnitTestRun(m) +} + +func resetPeriodicScanner() { + + if mainPeriodicScanner != nil { + select { + case <-mainPeriodicScanner.done: + // Already closed + default: + close(mainPeriodicScanner.done) + } + if mainPeriodicScanner.ticker != nil { + mainPeriodicScanner.ticker.Stop() + } + mainPeriodicScanner = nil + } +} + +func createTestSiteInfo(db *gorm.DB, interval int) error { + siteInfo := models.SiteInfo{ + InitialSetup: false, + PeriodicScanInterval: interval, + ConcurrentWorkers: 1, + } + return db.Create(&siteInfo).Error +} + +func TestGetPeriodicScanInterval(t *testing.T) { + db := test_utils.DatabaseTest(t) + + t.Run("successful retrieval", func(t *testing.T) { + assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval") + + duration, err := getPeriodicScanInterval(db) + assert.NoError(t, err, "Failed to retrieve periodic scan interval from database") + assert.Equal(t, 300*time.Second, duration, + "Periodic scan interval should be 300 seconds but got %v", duration) + }) + + t.Run("database error - no site info", func(t *testing.T) { + db.Exec("DELETE FROM site_info") + + duration, err := getPeriodicScanInterval(db) + assert.Error(t, err, "Expected error when no site info exists in database") + assert.Equal(t, time.Duration(0), duration, + "Duration should be zero when database error occurs, but got %v", duration) + }) +} + +func TestInitializePeriodicScanner(t *testing.T) { + db := test_utils.DatabaseTest(t) + + t.Run("successful initialization with injection", func(t *testing.T) { + defer resetPeriodicScanner() + + mockQueue := &MockScannerQueue{} + assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval") + assert.NoError(t, InitializePeriodicScannerWithQueue(db, mockQueue), + "Failed to initialize periodic scanner with mock queue") + + // Verify initialization + assert.NotNil(t, mainPeriodicScanner, "mainPeriodicScanner should not be nil after successful initialization") + assert.NotNil(t, mainPeriodicScanner.scannerQueue, "Scanner queue should not be nil after initialization") + assert.Equal(t, mockQueue, mainPeriodicScanner.scannerQueue, "Scanner should use the injected mock queue instance") + + // Verify ticker is set up + mainPeriodicScanner.tickerLocker.Lock() + tickerExists := mainPeriodicScanner.ticker != nil + mainPeriodicScanner.tickerLocker.Unlock() + assert.True(t, tickerExists, "Ticker should be created and set up after scanner initialization") + }) + + t.Run("backward compatibility with original function", func(t *testing.T) { + defer resetPeriodicScanner() + + assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval") + assert.NoError(t, InitializePeriodicScanner(db), + "Failed to initialize periodic scanner using original function") + + // Verify it uses RealScannerQueue + assert.NotNil(t, mainPeriodicScanner, + "mainPeriodicScanner should be initialized by original InitializePeriodicScanner function") + assert.IsType(t, &RealScannerQueue{}, mainPeriodicScanner.scannerQueue, + "Original InitializePeriodicScanner should use RealScannerQueue by default") + }) + + t.Run("double initialization error", func(t *testing.T) { + defer resetPeriodicScanner() + + mockQueue := &MockScannerQueue{} + assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval") + assert.NoError(t, InitializePeriodicScannerWithQueue(db, mockQueue), + "Failed first initialization for double initialization test") + + err := InitializePeriodicScannerWithQueue(db, mockQueue) + assert.Error(t, err, "Second initialization attempt should return an error") + assert.Contains(t, err.Error(), "already been initialized", + "Double initialization error should contain 'already been initialized' message") + }) +} + +func TestScanIntervalRunnerWithMocking(t *testing.T) { + t.Run("runner calls queue on ticker events", func(t *testing.T) { + mockQueue := &MockScannerQueue{} + + // Use a channel to synchronize and count calls + callChan := make(chan struct{}, 5) // Buffer for multiple calls + mockQueue.On("AddAllToQueue").Return(nil).Run(func(args mock.Arguments) { + select { + case callChan <- struct{}{}: + default: + // Channel full, but that's okay + } + }).Maybe() + + ps := &periodicScanner{ + ticker: time.NewTicker(50 * time.Millisecond), + ticker_changed: make(chan bool, 1), + done: make(chan struct{}), + tickerLocker: sync.Mutex{}, + scannerQueue: mockQueue, + } + + // Start runner + go ps.scanIntervalRunner() + + // Wait for at least one call with timeout + select { + case <-callChan: + // Success - at least one call received + case <-time.After(200 * time.Millisecond): + t.Fatal("Expected at least one call to AddAllToQueue within 200ms timeout, but ticker events were not processed") + } + + // Proper cleanup - stop ticker first, then close done + ps.ticker.Stop() + close(ps.done) + + // Give time for goroutine to finish + time.Sleep(50 * time.Millisecond) + + // Verify the queue was called as expected + mockQueue.AssertExpectations(t) + }) + + t.Run("runner handles queue errors gracefully", func(t *testing.T) { + mockQueue := &MockScannerQueue{} + // Mock queue to return an error + mockQueue.On("AddAllToQueue").Return(errors.New("queue error")).Maybe() + + ps := &periodicScanner{ + ticker: time.NewTicker(30 * time.Millisecond), + ticker_changed: make(chan bool, 1), + done: make(chan struct{}), + tickerLocker: sync.Mutex{}, + scannerQueue: mockQueue, + } + + // Start runner and let it run briefly + go ps.scanIntervalRunner() + time.Sleep(80 * time.Millisecond) + + // Proper cleanup + ps.ticker.Stop() + close(ps.done) + time.Sleep(50 * time.Millisecond) + + // Test passes if no panic occurred - errors should be logged gracefully + mockQueue.AssertExpectations(t) + }) + + t.Run("runner responds to shutdown signal", func(t *testing.T) { + mockQueue := &MockScannerQueue{} + + ps := &periodicScanner{ + ticker: nil, // No ticker to avoid timing issues + ticker_changed: make(chan bool, 1), + done: make(chan struct{}), + tickerLocker: sync.Mutex{}, + scannerQueue: mockQueue, + } + + runnerDone := make(chan bool) + go func() { + ps.scanIntervalRunner() + close(runnerDone) + }() + + close(ps.done) + + select { + case <-runnerDone: + // Success + case <-time.After(1 * time.Second): + t.Fatal("scanIntervalRunner goroutine did not exit within 1 second after closing done channel") + } + + mockQueue.AssertExpectations(t) + }) + + t.Run("runner responds to ticker changes", func(t *testing.T) { + mockQueue := &MockScannerQueue{} + + ps := &periodicScanner{ + ticker: nil, // Start without ticker + ticker_changed: make(chan bool, 1), + done: make(chan struct{}), + tickerLocker: sync.Mutex{}, + scannerQueue: mockQueue, + } + + runnerDone := make(chan bool) + go func() { + ps.scanIntervalRunner() + close(runnerDone) + }() + + // Send ticker change signal + select { + case ps.ticker_changed <- true: + case <-time.After(100 * time.Millisecond): + t.Fatal("Could not send ticker change signal within 100ms - channel may be blocked") + } + + // Clean shutdown + close(ps.done) + + // Wait for completion + select { + case <-runnerDone: + // Success + case <-time.After(500 * time.Millisecond): + t.Fatal("scanIntervalRunner did not exit within 500ms after receiving ticker change signal and shutdown") + } + + // Test passes if no deadlock occurs + mockQueue.AssertExpectations(t) + }) +} + +func TestRealScannerQueue(t *testing.T) { + t.Run("real scanner queue interface compliance", func(t *testing.T) { + queue := &RealScannerQueue{} + + // Just verify it implements the interface correctly + var _ ScannerQueue = queue + + // Test that it doesn't panic when created + assert.NotNil(t, queue, "RealScannerQueue instance should not be nil after creation") + + // We don't test the actual call since it requires external setup + }) +} diff --git a/api/scanner/scanner_album.go b/api/scanner/scanner_album.go new file mode 100644 index 0000000..e5af6da --- /dev/null +++ b/api/scanner/scanner_album.go @@ -0,0 +1,183 @@ +package scanner + +import ( + "fmt" + "os" + "path" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_tasks" + "github.com/photoview/photoview/api/scanner/scanner_utils" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +func NewRootAlbum(db *gorm.DB, rootPath string, owner *models.User) (*models.Album, error) { + + if !ValidRootPath(rootPath) { + return nil, ErrorInvalidRootPath + } + + if !path.IsAbs(rootPath) { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + + rootPath = path.Join(wd, rootPath) + } + + owners := []models.User{ + *owner, + } + + var matchedAlbums []models.Album + if err := db.Where("path_hash = ?", models.MD5Hash(rootPath)).Find(&matchedAlbums).Error; err != nil { + return nil, err + } + + if len(matchedAlbums) > 0 { + album := matchedAlbums[0] + + var matchedUserAlbumCount int64 + if err := db.Table("user_albums").Where("user_id = ?", owner.ID).Where("album_id = ?", album.ID).Count(&matchedUserAlbumCount).Error; err != nil { + return nil, err + } + + if matchedUserAlbumCount > 0 { + return nil, errors.New(fmt.Sprintf("user already owns a path containing this path: %s", rootPath)) + } + + if err := db.Model(&owner).Association("Albums").Append(&album); err != nil { + return nil, errors.Wrap(err, "add owner to already existing album") + } + + return &album, nil + } else { + album := models.Album{ + Title: path.Base(rootPath), + Path: rootPath, + Owners: owners, + } + + if err := db.Create(&album).Error; err != nil { + return nil, err + } + + return &album, nil + } +} + +var ErrorInvalidRootPath = errors.New("invalid root path") + +func ValidRootPath(rootPath string) bool { + _, err := os.Stat(rootPath) + if err != nil { + log.Warn(nil, "invalid root path", "root_path", rootPath, "error", err) + return false + } + + return true +} + +func ScanAlbum(ctx scanner_task.TaskContext) error { + newCtx, err := scanner_tasks.Tasks.BeforeScanAlbum(ctx) + if err != nil { + return errors.Wrapf(err, "before scan album (%s)", ctx.GetAlbum().Path) + } + ctx = newCtx + + // Scan for photos + albumMedia, err := findMediaForAlbum(ctx) + if err != nil { + return errors.Wrapf(err, "find media for album (%s): %s", ctx.GetAlbum().Path, err) + } + + changedMedia := make([]*models.Media, 0) + for i, media := range albumMedia { + mediaData := media_encoding.NewEncodeMediaData(media) + + if err := scanMedia(ctx, media, &mediaData, i, len(albumMedia)); err != nil { + scanner_utils.ScannerError(ctx, "Error scanning media for album (%d) file (%s): %s\n", ctx.GetAlbum().ID, media.Path, err) + } + } + + if err := scanner_tasks.Tasks.AfterScanAlbum(ctx, changedMedia, albumMedia); err != nil { + return errors.Wrap(err, "after scan album") + } + + return nil +} + +func findMediaForAlbum(ctx scanner_task.TaskContext) ([]*models.Media, error) { + + albumMedia := make([]*models.Media, 0) + + dirContent, err := os.ReadDir(ctx.GetAlbum().Path) + if err != nil { + return nil, err + } + + for _, item := range dirContent { + mediaPath := path.Join(ctx.GetAlbum().Path, item.Name()) + log.Info(ctx, "Check the media", "media_path", mediaPath) + + isDirSymlink, err := utils.IsDirSymlink(mediaPath) + if err != nil { + log.Warn(ctx, "Cannot detect whether the path is symlink to a directory. Pretending it is not", "media_path", mediaPath) + isDirSymlink = false + } + + if !item.IsDir() && !isDirSymlink && ctx.GetCache().IsPathMedia(mediaPath) { + itemInfo, err := item.Info() + if err != nil { + return nil, err + } + skip, err := scanner_tasks.Tasks.MediaFound(ctx, itemInfo, mediaPath) + if err != nil { + return nil, err + } + if skip { + continue + } + + err = ctx.DatabaseTransaction(func(ctx scanner_task.TaskContext) error { + media, isNewMedia, err := ScanMedia(ctx.GetDB(), mediaPath, ctx.GetAlbum().ID, ctx.GetCache()) + if err != nil { + return errors.Wrapf(err, "scanning media error (%s)", mediaPath) + } + + if err = scanner_tasks.Tasks.AfterMediaFound(ctx, media, isNewMedia); err != nil { + return err + } + + albumMedia = append(albumMedia, media) + + return nil + }) + + if err != nil { + scanner_utils.ScannerError(ctx, "Error scanning media for album (%d): %s\n", ctx.GetAlbum().ID, err) + continue + } + } + + } + + return albumMedia, nil +} + +func processMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData) ([]*models.MediaURL, error) { + + // Make sure media cache directory exists + mediaCachePath, err := mediaData.Media.CachePath() + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "cache directory error") + } + + return scanner_tasks.Tasks.ProcessMedia(ctx, mediaData, mediaCachePath) +} diff --git a/api/scanner/scanner_album_test.go b/api/scanner/scanner_album_test.go new file mode 100644 index 0000000..4e37c15 --- /dev/null +++ b/api/scanner/scanner_album_test.go @@ -0,0 +1,74 @@ +package scanner_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +const testDataPath = "./test_media/library" + +func TestNewRootPath(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user := models.User{ + Username: "user1", + } + + if !assert.NoError(t, db.Save(&user).Error) { + return + } + + t.Run("Insert valid root album", func(t *testing.T) { + album, err := scanner.NewRootAlbum(db, testDataPath, &user) + if !assert.NoError(t, err) { + return + } + + assert.NotNil(t, album) + assert.Contains(t, album.Path, "/api/scanner/test_media") + assert.NotEmpty(t, album.Owners) + }) + + t.Run("Insert duplicate root album", func(t *testing.T) { + + _, err := scanner.NewRootAlbum(db, testDataPath, &user) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "user already owns a path containing this path:") + }) + + t.Run("Insert invalid root album", func(t *testing.T) { + + _, err := scanner.NewRootAlbum(db, "./invalid_path", &user) + + assert.Error(t, err) + assert.Equal(t, err.Error(), "invalid root path") + }) + + t.Run("Add existing root album to new user", func(t *testing.T) { + + user2 := models.User{ + Username: "user2", + } + + if !assert.NoError(t, db.Save(&user2).Error) { + return + } + + album, err := scanner.NewRootAlbum(db, testDataPath, &user2) + if !assert.NoError(t, err) { + return + } + + assert.NotNil(t, album) + assert.Contains(t, album.Path, "/api/scanner/test_media") + + ownerCount := db.Model(&album).Association("Owners").Count() + assert.EqualValues(t, 2, ownerCount) + }) + +} diff --git a/api/scanner/scanner_cache/cache.go b/api/scanner/scanner_cache/cache.go new file mode 100644 index 0000000..9af93bd --- /dev/null +++ b/api/scanner/scanner_cache/cache.go @@ -0,0 +1,128 @@ +package scanner_cache + +import ( + "log" + "os" + "path" + "sync" + + "github.com/photoview/photoview/api/scanner/media_type" +) + +type AlbumScannerCache struct { + path_contains_photos map[string]bool + photo_types map[string]media_type.MediaType + ignore_data map[string][]string + mutex sync.Mutex +} + +func MakeAlbumCache() *AlbumScannerCache { + return &AlbumScannerCache{ + path_contains_photos: make(map[string]bool), + photo_types: make(map[string]media_type.MediaType), + ignore_data: make(map[string][]string), + } +} + +// Insert single album directory in cache +func (c *AlbumScannerCache) InsertAlbumPath(path string, containsPhoto bool) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.path_contains_photos[path] = containsPhoto +} + +// Insert album path and all parent directories up to the given root directory in cache +func (c *AlbumScannerCache) InsertAlbumPaths(endPath string, root string, containsPhoto bool) { + currPath := path.Clean(endPath) + rootPath := path.Clean(root) + + c.mutex.Lock() + defer c.mutex.Unlock() + + for currPath != rootPath || currPath == "." { + + c.path_contains_photos[currPath] = containsPhoto + + currPath = path.Dir(currPath) + } +} + +func (c *AlbumScannerCache) AlbumContainsPhotos(path string) *bool { + c.mutex.Lock() + defer c.mutex.Unlock() + + contains_photo, found := c.path_contains_photos[path] + if found { + // log.Printf("Album cache hit: %s\n", path) + return &contains_photo + } + + return nil +} + +// func (c *AlbumScannerCache) InsertPhotoType(path string, content_type MediaType) { +// c.mutex.Lock() +// defer c.mutex.Unlock() + +// (c.photo_types)[path] = content_type +// } + +func (c *AlbumScannerCache) GetMediaType(path string) media_type.MediaType { + c.mutex.Lock() + defer c.mutex.Unlock() + + result, found := c.photo_types[path] + if found { + return result + } + + mediaType := media_type.GetMediaType(path) + if mediaType == media_type.TypeUnknown { + return mediaType + } + + c.photo_types[path] = mediaType + + return mediaType +} + +func (c *AlbumScannerCache) GetAlbumIgnore(path string) *[]string { + c.mutex.Lock() + defer c.mutex.Unlock() + + ignore_data, found := c.ignore_data[path] + if found { + return &ignore_data + } + + return nil +} + +func (c *AlbumScannerCache) InsertAlbumIgnore(path string, ignoreData []string) { + c.mutex.Lock() + defer c.mutex.Unlock() + + c.ignore_data[path] = ignoreData +} + +func (c *AlbumScannerCache) IsPathMedia(mediaPath string) bool { + mediaType := c.GetMediaType(mediaPath) + + // Ignore hidden files + if path.Base(mediaPath)[0:1] == "." { + return false + } + + if !mediaType.IsSupported() { + log.Printf("Unsupported media type %q for file: %s\n", mediaType, mediaPath) + return false + } + + // Make sure file isn't empty + fileStats, err := os.Stat(mediaPath) + if err != nil || fileStats.Size() == 0 { + return false + } + + return true +} diff --git a/api/scanner/scanner_media.go b/api/scanner/scanner_media.go new file mode 100644 index 0000000..d69e3ff --- /dev/null +++ b/api/scanner/scanner_media.go @@ -0,0 +1,93 @@ +package scanner + +import ( + "context" + "fmt" + "log" + "os" + "path" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/media_type" + "github.com/photoview/photoview/api/scanner/scanner_cache" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +var ProcessSingleMediaFunc = ProcessSingleMedia + +func ScanMedia(tx *gorm.DB, mediaPath string, albumId int, cache *scanner_cache.AlbumScannerCache) (*models.Media, bool, error) { + mediaName := path.Base(mediaPath) + + // Check if media already exists + { + var media []*models.Media + + result := tx.Where("path_hash = ?", models.MD5Hash(mediaPath)).Find(&media) + + if result.Error != nil { + return nil, false, errors.Wrap(result.Error, "scan media fetch from database") + } + + if result.RowsAffected > 0 { + // log.Printf("Media already scanned: %s\n", mediaPath) + return media[0], false, nil + } + } + + log.Printf("Scanning media: %s\n", mediaPath) + + mediaType := cache.GetMediaType(mediaPath) + if mediaType == media_type.TypeUnknown { + return nil, false, fmt.Errorf("could not determine if media %s of type %s was photo or video", mediaPath, mediaType) + } + + var mediaTypeText models.MediaType + + if mediaType.IsVideo() { + mediaTypeText = models.MediaTypeVideo + } else { + mediaTypeText = models.MediaTypePhoto + } + + stat, err := os.Stat(mediaPath) + if err != nil { + return nil, false, err + } + + media := models.Media{ + Title: mediaName, + Path: mediaPath, + AlbumID: albumId, + Type: mediaTypeText, + DateShot: stat.ModTime(), + } + + if err := tx.Create(&media).Error; err != nil { + return nil, false, errors.Wrap(err, "could not insert media into database") + } + + return &media, true, nil +} + +// ProcessSingleMedia processes a single media, might be used to reprocess media with corrupted cache +// Function waits for processing to finish before returning. +func ProcessSingleMedia(ctx context.Context, db *gorm.DB, media *models.Media) error { + albumCache := scanner_cache.MakeAlbumCache() + + var album models.Album + if err := db.Model(media).Association("Album").Find(&album); err != nil { + return err + } + + mediaData := media_encoding.NewEncodeMediaData(media) + + taskContext := scanner_task.NewTaskContext(ctx, db, &album, albumCache) + if err := scanMedia(taskContext, media, &mediaData, 0, 1); err != nil { + return errors.Wrap(err, "single media scan") + } + + return nil +} diff --git a/api/scanner/scanner_queue/queue.go b/api/scanner/scanner_queue/queue.go new file mode 100644 index 0000000..f7bd9db --- /dev/null +++ b/api/scanner/scanner_queue/queue.go @@ -0,0 +1,264 @@ +package scanner_queue + +import ( + "context" + "fmt" + "log" + "sync" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/notification" + "github.com/photoview/photoview/api/scanner" + "github.com/photoview/photoview/api/scanner/scanner_cache" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_utils" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const globalScannerProgress = "global-scanner-progress" + +// ScannerJob describes a job on the queue to be run by the scanner over a single album +type ScannerJob struct { + ctx scanner_task.TaskContext + // album *models.Album + // cache *scanner_cache.AlbumScannerCache +} + +func NewScannerJob(ctx scanner_task.TaskContext) ScannerJob { + return ScannerJob{ + ctx, + } +} + +func (job *ScannerJob) Run(db *gorm.DB) { + err := scanner.ScanAlbum(job.ctx) + if err != nil { + scanner_utils.ScannerError(nil, "Failed to scan album: %v", err) + } +} + +type ScannerQueueSettings struct { + max_concurrent_tasks int +} + +type ScannerQueue struct { + mutex sync.Mutex + idle_chan chan bool + in_progress []ScannerJob + up_next []ScannerJob + db *gorm.DB + settings ScannerQueueSettings + close_chan *chan bool + running bool +} + +var global_scanner_queue ScannerQueue + +func InitializeScannerQueue(db *gorm.DB) error { + + var concurrentWorkers int + { + site_info, err := models.GetSiteInfo(db) + if err != nil { + return errors.Wrap(err, "get current workers from database") + } + concurrentWorkers = site_info.ConcurrentWorkers + } + + log.Printf("Initializing scanner queue with %d workers", concurrentWorkers) + + global_scanner_queue = ScannerQueue{ + idle_chan: make(chan bool, 1), + in_progress: make([]ScannerJob, 0), + up_next: make([]ScannerJob, 0), + db: db, + settings: ScannerQueueSettings{max_concurrent_tasks: concurrentWorkers}, + close_chan: nil, + running: true, + } + + go global_scanner_queue.startBackgroundWorker() + + return nil +} + +func CloseScannerQueue() { + global_scanner_queue.CloseBackgroundWorker() +} + +func ChangeScannerConcurrentWorkers(newMaxWorkers int) { + global_scanner_queue.mutex.Lock() + defer global_scanner_queue.mutex.Unlock() + + log.Printf("Scanner max concurrent workers changed to: %d", newMaxWorkers) + global_scanner_queue.settings.max_concurrent_tasks = newMaxWorkers +} + +func (queue *ScannerQueue) startBackgroundWorker() { + + notifyThrottle := utils.NewThrottle(500 * time.Millisecond) + + for { + log.Println("Queue waiting") + <-queue.idle_chan + + queue.mutex.Lock() + shouldStop := queue.close_chan != nil && len(queue.in_progress) == 0 && len(queue.up_next) == 0 + queue.running = false + queue.mutex.Unlock() + + if shouldStop { + *queue.close_chan <- true + break + } + + queue.processQueue(¬ifyThrottle) + } + + log.Println("Scanner background worker stopped") +} + +func (queue *ScannerQueue) CloseBackgroundWorker() { + queue.mutex.Lock() + closeChan := make(chan bool) + queue.close_chan = &closeChan + queue.mutex.Unlock() + + queue.notify() + + log.Println("Waiting for scanner background worker to finish all jobs...") + <-closeChan +} + +func (queue *ScannerQueue) processQueue(notifyThrottle *utils.Throttle) { + log.Println("Queue waiting for lock") + queue.mutex.Lock() + maxJobs := queue.settings.max_concurrent_tasks + log.Printf("Queue running: in_progress: %d, max_tasks: %d, queue_len: %d\n", len(queue.in_progress), maxJobs, len(queue.up_next)) + + for len(queue.in_progress) < maxJobs && len(queue.up_next) > 0 { + log.Println("Queue starting job") + nextJob := queue.up_next[0] + queue.up_next = queue.up_next[1:] + queue.in_progress = append(queue.in_progress, nextJob) + jobNum := len(queue.in_progress) + + go func() { + log.Printf("Starting job %d/%d\n", jobNum, maxJobs) + nextJob.Run(queue.db) + log.Printf("Finished job %d/%d\n", jobNum, maxJobs) + + // Delete finished job from queue + queue.mutex.Lock() + for i, x := range queue.in_progress { + if x == nextJob { + queue.in_progress[i] = queue.in_progress[len(queue.in_progress)-1] + queue.in_progress = queue.in_progress[0 : len(queue.in_progress)-1] + break + } + } + queue.mutex.Unlock() + + queue.notify() + }() + } + + inProgressLength := len(global_scanner_queue.in_progress) + upNextLength := len(global_scanner_queue.up_next) + + queue.mutex.Unlock() + + if inProgressLength+upNextLength == 0 { + notification.BroadcastNotification(&models.Notification{ + Key: globalScannerProgress, + Type: models.NotificationTypeMessage, + Header: "Scanner complete", + Content: "All jobs have been scanned", + Positive: true, + }) + } else { + notifyThrottle.Trigger(func() { + notification.BroadcastNotification(&models.Notification{ + Key: globalScannerProgress, + Type: models.NotificationTypeMessage, + Header: "Scanning media", + Content: fmt.Sprintf("%d jobs in progress\n%d jobs waiting", inProgressLength, upNextLength), + }) + }) + } +} + +// Notifies the queue that the jobs has changed +func (queue *ScannerQueue) notify() bool { + select { + case queue.idle_chan <- true: + return true + default: + return false + } +} + +func AddAllToQueue() error { + + var users []*models.User + result := global_scanner_queue.db.Find(&users) + if result.Error != nil { + return errors.Wrap(result.Error, "get all users from database") + } + + for _, user := range users { + if err := AddUserToQueue(user); err != nil { + return errors.Wrapf(err, "failed to add user for scanning (%d)", user.ID) + } + } + + return nil +} + +// AddUserToQueue finds all root albums owned by the given user and adds them to the scanner queue. +// Function does not block. +func AddUserToQueue(user *models.User) error { + albumCache := scanner_cache.MakeAlbumCache() + albums, album_errors := scanner.FindAlbumsForUser(global_scanner_queue.db, user, albumCache) + for _, err := range album_errors { + return errors.Wrapf(err, "find albums for user (user_id: %d)", user.ID) + } + + global_scanner_queue.mutex.Lock() + for _, album := range albums { + global_scanner_queue.addJob(&ScannerJob{ + ctx: scanner_task.NewTaskContext(context.Background(), global_scanner_queue.db, album, albumCache), + }) + } + global_scanner_queue.mutex.Unlock() + + return nil +} + +// Queue should be locked prior to calling this function +func (queue *ScannerQueue) addJob(job *ScannerJob) error { + if exists, err := queue.jobOnQueue(job); exists || err != nil { + return err + } + queue.up_next = append(queue.up_next, *job) + queue.notify() + + return nil +} + +// Queue should be locked prior to calling this function +func (queue *ScannerQueue) jobOnQueue(job *ScannerJob) (bool, error) { + + scannerJobs := append(queue.in_progress, queue.up_next...) + + for _, scannerJob := range scannerJobs { + if scannerJob.ctx.GetAlbum().ID == job.ctx.GetAlbum().ID { + return true, nil + } + } + + return false, nil +} diff --git a/api/scanner/scanner_queue/queue_test.go b/api/scanner/scanner_queue/queue_test.go new file mode 100644 index 0000000..f2280ab --- /dev/null +++ b/api/scanner/scanner_queue/queue_test.go @@ -0,0 +1,109 @@ +package scanner_queue + +import ( + "context" + "flag" + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/scanner_cache" + "github.com/photoview/photoview/api/scanner/scanner_task" +) + +var _ = flag.Bool("database", false, "run database integration tests") +var _ = flag.Bool("filesystem", false, "run filesystem integration tests") + +func makeAlbumWithID(id int) *models.Album { + var album models.Album + album.ID = id + + return &album +} + +func makeScannerJob(albumID int) ScannerJob { + return NewScannerJob(scanner_task.NewTaskContext(context.Background(), nil, makeAlbumWithID(albumID), scanner_cache.MakeAlbumCache())) +} + +func TestScannerQueueAddJob(t *testing.T) { + + scannerJobs := []ScannerJob{ + makeScannerJob(100), + makeScannerJob(20), + } + + mockScannerQueue := ScannerQueue{ + idle_chan: make(chan bool, 1), + in_progress: make([]ScannerJob, 0), + up_next: scannerJobs, + db: nil, + } + + t.Run("add new job to scanner queue", func(t *testing.T) { + newJob := makeScannerJob(42) + + startingJobs := len(mockScannerQueue.up_next) + + err := mockScannerQueue.addJob(&newJob) + if err != nil { + t.Errorf(".AddJob() returned an unexpected error: %s", err) + } + + if len(mockScannerQueue.up_next) != startingJobs+1 { + t.Errorf("Expected scanner queue length to be %d but got %d", startingJobs+1, len(mockScannerQueue.up_next)) + } else if mockScannerQueue.up_next[len(mockScannerQueue.up_next)-1] != newJob { + t.Errorf("Expected scanner queue to contain the job that was added: %+v", newJob) + } + + }) + + t.Run("add existing job to scanner queue", func(t *testing.T) { + startingJobs := len(mockScannerQueue.up_next) + + job := makeScannerJob(20) + err := mockScannerQueue.addJob(&job) + if err != nil { + t.Errorf(".AddJob() returned an unexpected error: %s", err) + } + + if len(mockScannerQueue.up_next) != startingJobs { + t.Errorf("Expected scanner queue length not to change: start length %d, new length %d", startingJobs, len(mockScannerQueue.up_next)) + } + + }) +} + +func TestScannerQueueJobOnQueue(t *testing.T) { + + scannerJobs := []ScannerJob{ + makeScannerJob(100), + makeScannerJob(20), + } + + mockScannerQueue := ScannerQueue{ + idle_chan: make(chan bool, 1), + in_progress: make([]ScannerJob, 0), + up_next: scannerJobs, + db: nil, + } + + onQueueTests := []struct { + string + bool + ScannerJob + }{ + {"album which owner is already on the queue", true, makeScannerJob(100)}, + {"album that is not on the queue", false, makeScannerJob(321)}, + } + + for _, test := range onQueueTests { + t.Run(test.string, func(t *testing.T) { + onQueue, err := mockScannerQueue.jobOnQueue(&test.ScannerJob) + if err != nil { + t.Error("Expected jobOnQueue not to return an error") + } else if onQueue != test.bool { + t.Fail() + } + }) + } + +} diff --git a/api/scanner/scanner_task/scanner_task.go b/api/scanner/scanner_task/scanner_task.go new file mode 100644 index 0000000..cfff88f --- /dev/null +++ b/api/scanner/scanner_task/scanner_task.go @@ -0,0 +1,90 @@ +package scanner_task + +import ( + "context" + "database/sql" + "flag" + "io/fs" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_cache" + "gorm.io/gorm" +) + +// ScannerTask is an interface for a task to be performed as a part of the scanner pipeline +type ScannerTask interface { + // BeforeScanAlbum will run at the beginning of the scan task. + // New values can be stored in the returned TaskContext that will live throughout the lifetime of the task. + BeforeScanAlbum(ctx TaskContext) (TaskContext, error) + + // AfterScanAlbum will run at the end of the scan task. + AfterScanAlbum(ctx TaskContext, changedMedia []*models.Media, albumMedia []*models.Media) error + + // MediaFound will run for each media file found on the filesystem. + // It will run even when the media is already present in the database. + // If the returned skip value is true, the media will be skipped and further steps will not be executed for the given file. + MediaFound(ctx TaskContext, fileInfo fs.FileInfo, mediaPath string) (skip bool, err error) + + // AfterMediaFound will run each media file after is has been saved to the database, but not processed yet. + // It will run even when the media is already present in the database, in that case `newMedia` will be true. + AfterMediaFound(ctx TaskContext, media *models.Media, newMedia bool) error + + BeforeProcessMedia(ctx TaskContext, mediaData *media_encoding.EncodeMediaData) (TaskContext, error) + ProcessMedia(ctx TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) (updatedURLs []*models.MediaURL, err error) + AfterProcessMedia(ctx TaskContext, mediaData *media_encoding.EncodeMediaData, updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error +} + +type TaskContext struct { + context.Context +} + +func NewTaskContext(parent context.Context, db *gorm.DB, album *models.Album, cache *scanner_cache.AlbumScannerCache) TaskContext { + ctx := TaskContext{Context: parent} + ctx = ctx.WithValue(taskCtxKeyAlbum, album) + ctx = ctx.WithValue(taskCtxKeyAlbumCache, cache) + ctx = ctx.WithDB(db) + + return ctx +} + +type taskCtxKeyType string + +const ( + taskCtxKeyAlbum taskCtxKeyType = "task_album" + taskCtxKeyAlbumCache taskCtxKeyType = "task_album_cache" + taskCtxKeyDatabase taskCtxKeyType = "task_database" +) + +func (c TaskContext) GetAlbum() *models.Album { + return c.Context.Value(taskCtxKeyAlbum).(*models.Album) +} + +func (c TaskContext) GetCache() *scanner_cache.AlbumScannerCache { + return c.Context.Value(taskCtxKeyAlbumCache).(*scanner_cache.AlbumScannerCache) +} + +func (c TaskContext) GetDB() *gorm.DB { + return c.Context.Value(taskCtxKeyDatabase).(*gorm.DB) +} + +func (c TaskContext) DatabaseTransaction(transFunc func(ctx TaskContext) error, opts ...*sql.TxOptions) error { + return c.GetDB().Transaction(func(tx *gorm.DB) error { + return transFunc(c.WithDB(tx)) + }, opts...) +} + +func (c TaskContext) WithValue(key, val interface{}) TaskContext { + return TaskContext{ + Context: context.WithValue(c.Context, key, val), + } +} + +func (c TaskContext) WithDB(db *gorm.DB) TaskContext { + // Allow db to be nil in tests + if db == nil && flag.Lookup("test.v") != nil { + return c + } + + return c.WithValue(taskCtxKeyDatabase, db.WithContext(c.Context)) +} diff --git a/api/scanner/scanner_task/scanner_task_base.go b/api/scanner/scanner_task/scanner_task_base.go new file mode 100644 index 0000000..4864fcf --- /dev/null +++ b/api/scanner/scanner_task/scanner_task_base.go @@ -0,0 +1,39 @@ +package scanner_task + +import ( + "io/fs" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" +) + +// ScannerTaskBase provides a default "empty" implementation of ScannerTask, +type ScannerTaskBase struct{} + +func (t ScannerTaskBase) BeforeScanAlbum(ctx TaskContext) (TaskContext, error) { + return ctx, nil +} + +func (t ScannerTaskBase) AfterScanAlbum(ctx TaskContext, changedMedia []*models.Media, albumMedia []*models.Media) error { + return nil +} + +func (t ScannerTaskBase) MediaFound(ctx TaskContext, fileInfo fs.FileInfo, mediaPath string) (skip bool, err error) { + return false, nil +} + +func (t ScannerTaskBase) AfterMediaFound(ctx TaskContext, media *models.Media, newMedia bool) error { + return nil +} + +func (t ScannerTaskBase) BeforeProcessMedia(ctx TaskContext, mediaData *media_encoding.EncodeMediaData) (TaskContext, error) { + return ctx, nil +} + +func (t ScannerTaskBase) ProcessMedia(ctx TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) (updatedURLs []*models.MediaURL, err error) { + return []*models.MediaURL{}, nil +} + +func (t ScannerTaskBase) AfterProcessMedia(ctx TaskContext, mediaData *media_encoding.EncodeMediaData, updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error { + return nil +} diff --git a/api/scanner/scanner_tasks/blurhash_task.go b/api/scanner/scanner_tasks/blurhash_task.go new file mode 100644 index 0000000..6cbee79 --- /dev/null +++ b/api/scanner/scanner_tasks/blurhash_task.go @@ -0,0 +1,87 @@ +package scanner_tasks + +import ( + "fmt" + "image" + _ "image/jpeg" + "os" + + "github.com/buckket/go-blurhash" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_task" +) + +type BlurhashTask struct { + scanner_task.ScannerTaskBase +} + +func (t BlurhashTask) AfterProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error { + hasThumbnailUpdated := false + for _, url := range updatedURLs { + if url.Purpose == models.PhotoThumbnail || url.Purpose == models.VideoThumbnail { + hasThumbnailUpdated = true + break + } + } + + var media *models.Media + if err := ctx.GetDB().Preload("MediaURL").Where("id = ?", mediaData.Media.ID).First(&media).Error; err != nil { + return fmt.Errorf("failed to get media(id:%d): %w", mediaData.Media.ID, err) + } + + if media.Blurhash != nil && !hasThumbnailUpdated { + log.Info(ctx, "No thumbnail updated, ignore generating blurhash", "media", media.Path) + return nil + } + + thumbnail, err := media.GetThumbnail() + if err != nil { + return fmt.Errorf("failed to get thumbnail of image %q: %w", mediaData.Media.Path, err) + } + + hashStr, err := generateBlurhashFromThumbnail(thumbnail) + if err != nil { + return fmt.Errorf("failed to generate blurhash of image %q: %w", mediaData.Media.Path, err) + } + + media.Blurhash = &hashStr + if err := ctx.GetDB().Select("blurhash").Save(media).Error; err != nil { + return fmt.Errorf("failed to store blurhash of image %q: %w", mediaData.Media.Path, err) + } + + log.Info(ctx, "Generated blurhash of image", "media", mediaData.Media.Path) + + return nil +} + +// generateBlurhashFromThumbnail generates a blurhash for a single media and stores it in the database +func generateBlurhashFromThumbnail(thumbnail *models.MediaURL) (string, error) { + path, err := thumbnail.CachedPath() + if err != nil { + return "", fmt.Errorf("get path of media(id:%d) error: %w", thumbnail.MediaID, err) + } + + f, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("open %q error: %w", path, err) + } + defer f.Close() + + imageData, _, err := image.Decode(f) + if err != nil { + return "", fmt.Errorf("decode %q error: %w", path, err) + } + + const ( + componentX = 4 + componentY = 3 + ) + hashStr, err := blurhash.Encode(componentX, componentY, imageData) + if err != nil { + return "", fmt.Errorf("encode blurhash of %q error: %w", path, err) + } + + return hashStr, nil +} diff --git a/api/scanner/scanner_tasks/cleanup_tasks/cleanup_media.go b/api/scanner/scanner_tasks/cleanup_tasks/cleanup_media.go new file mode 100644 index 0000000..a021d57 --- /dev/null +++ b/api/scanner/scanner_tasks/cleanup_tasks/cleanup_media.go @@ -0,0 +1,136 @@ +package cleanup_tasks + +import ( + "os" + "path" + "strconv" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" + "github.com/photoview/photoview/api/scanner/scanner_utils" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// CleanupMedia removes media entries from the database that are no longer present on the filesystem +func CleanupMedia(db *gorm.DB, albumId int, albumMedia []*models.Media) []error { + albumMediaIds := make([]int, len(albumMedia)) + for i, media := range albumMedia { + albumMediaIds[i] = media.ID + } + + // Will get from database + var mediaList []models.Media + + query := db.Where("album_id = ?", albumId) + + // Select media from database that was not found on hard disk + if len(albumMedia) > 0 { + query = query.Where("NOT id IN (?)", albumMediaIds) + } + + if err := query.Find(&mediaList).Error; err != nil { + return []error{errors.Wrap(err, "get media files to be deleted from database")} + } + + deleteErrors := make([]error, 0) + + mediaIDs := make([]int, 0) + for _, media := range mediaList { + + mediaIDs = append(mediaIDs, media.ID) + cachePath := path.Join(utils.MediaCachePath(), strconv.Itoa(int(albumId)), strconv.Itoa(int(media.ID))) + err := os.RemoveAll(cachePath) + if err != nil { + deleteErrors = append(deleteErrors, errors.Wrapf(err, "delete unused cache folder (%s)", cachePath)) + } + + } + + if len(mediaIDs) > 0 { + if err := db.Where("id IN (?)", mediaIDs).Delete(models.Media{}).Error; err != nil { + deleteErrors = append(deleteErrors, errors.Wrap(err, "delete old media from database")) + } + + // Reload faces after deleting media + if face_detection.GlobalFaceDetector != nil { + if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(db); err != nil { + deleteErrors = append(deleteErrors, errors.Wrap(err, "reload faces from database")) + } + } + } + + return deleteErrors +} + +// DeleteOldUserAlbums finds and deletes old albums in the database and cache that does not exist on the filesystem anymore. +func DeleteOldUserAlbums(db *gorm.DB, scannedAlbums []*models.Album, user *models.User) []error { + if len(scannedAlbums) == 0 { + return nil + } + + scannedAlbumIDs := make([]interface{}, len(scannedAlbums)) + for i, album := range scannedAlbums { + scannedAlbumIDs[i] = album.ID + } + + // Old albums to be deleted + var deleteAlbums []models.Album + + // Find old albums in database + query := db. + Select("albums.*"). + Table("user_albums"). + Joins("JOIN albums ON user_albums.album_id = albums.id"). + Where("user_id = ?", user.ID). + Where("album_id NOT IN (?)", scannedAlbumIDs) + + if err := query.Find(&deleteAlbums).Error; err != nil { + return []error{errors.Wrap(err, "get albums to be deleted from database")} + } + + if len(deleteAlbums) == 0 { + return []error{} + } + + deleteErrors := make([]error, 0) + + // Delete old albums from cache + deleteAlbumIDs := make([]int, len(deleteAlbums)) + for i, album := range deleteAlbums { + deleteAlbumIDs[i] = album.ID + cachePath := path.Join(utils.MediaCachePath(), strconv.Itoa(int(album.ID))) + err := os.RemoveAll(cachePath) + if err != nil { + deleteErrors = append(deleteErrors, errors.Wrapf(err, "delete unused cache folder (%s)", cachePath)) + } + } + + // Delete old albums from database + err := db.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("album_id IN (?)", deleteAlbumIDs).Delete(&models.UserAlbums{}).Error; err != nil { + return err + } + + if err := tx.Where("id IN (?)", deleteAlbumIDs).Delete(models.Album{}).Error; err != nil { + return err + } + + return nil + }) + + if err != nil { + scanner_utils.ScannerError(nil, "Could not delete old albums from database:\n%s\n", err) + deleteErrors = append(deleteErrors, err) + } + + // Reload faces after deleting albums + if face_detection.GlobalFaceDetector != nil { + if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(db); err != nil { + deleteErrors = append(deleteErrors, err) + } + } + + return deleteErrors +} diff --git a/api/scanner/scanner_tasks/cleanup_tasks/cleanup_media_test.go b/api/scanner/scanner_tasks/cleanup_tasks/cleanup_media_test.go new file mode 100644 index 0000000..28bad48 --- /dev/null +++ b/api/scanner/scanner_tasks/cleanup_tasks/cleanup_media_test.go @@ -0,0 +1,105 @@ +package cleanup_tasks_test + +import ( + "os" + "path" + "testing" + + "github.com/otiai10/copy" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" + "github.com/photoview/photoview/api/test_utils" + scanner_utils "github.com/photoview/photoview/api/test_utils/scanner" + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} + +func TestCleanupMedia(t *testing.T) { + test_utils.FilesystemTest(t) + db := test_utils.DatabaseTest(t) + + if !assert.NoError(t, face_detection.InitializeFaceDetector(db)) { + return + } + + testDir := t.TempDir() + assert.NoError(t, copy.Copy("../../test_media/library", testDir)) + + countAllMedia := func() int { + var allMedia []*models.Media + if !assert.NoError(t, db.Find(&allMedia).Error) { + return -1 + } + return len(allMedia) + } + + countAllMediaURLs := func() int { + var allMediaURLs []*models.MediaURL + if !assert.NoError(t, db.Find(&allMediaURLs).Error) { + return -1 + } + return len(allMediaURLs) + } + + pass := "1234" + user1, err := models.RegisterUser(db, "user1", &pass, true) + if !assert.NoError(t, err) { + return + } + + user2, err := models.RegisterUser(db, "user2", &pass, true) + if !assert.NoError(t, err) { + return + } + + rootAlbum := models.Album{ + Title: "root album", + Path: testDir, + } + + if !assert.NoError(t, db.Save(&rootAlbum).Error) { + return + } + + err = db.Model(user1).Association("Albums").Append(&rootAlbum) + if !assert.NoError(t, err) { + return + } + err = db.Model(user2).Association("Albums").Append(&rootAlbum) + if !assert.NoError(t, err) { + return + } + + t.Run("Modify albums", func(t *testing.T) { + scanner_utils.RunScannerOnUser(t, db, user1) + assert.Equal(t, 9, countAllMedia()) + assert.Equal(t, 18, countAllMediaURLs()) + + // move faces directory + assert.NoError(t, os.Rename(path.Join(testDir, "faces"), path.Join(testDir, "faces_moved"))) + scanner_utils.RunScannerAll(t, db) + assert.Equal(t, 9, countAllMedia()) + assert.Equal(t, 18, countAllMediaURLs()) + + // remove faces_moved directory + assert.NoError(t, os.RemoveAll(path.Join(testDir, "faces_moved"))) + scanner_utils.RunScannerAll(t, db) + assert.Equal(t, 3, countAllMedia()) + assert.Equal(t, 6, countAllMediaURLs()) + }) + + t.Run("Modify images", func(t *testing.T) { + assert.NoError(t, os.Rename(path.Join(testDir, "buttercup_close_summer_yellow.jpg"), path.Join(testDir, "yellow-flower.jpg"))) + scanner_utils.RunScannerAll(t, db) + assert.Equal(t, 3, countAllMedia()) + assert.Equal(t, 6, countAllMediaURLs()) + + assert.NoError(t, os.Remove(path.Join(testDir, "lilac_lilac_bush_lilac.jpg"))) + scanner_utils.RunScannerAll(t, db) + assert.Equal(t, 2, countAllMedia()) + assert.Equal(t, 4, countAllMediaURLs()) + }) +} diff --git a/api/scanner/scanner_tasks/cleanup_tasks/media_cleanup_task.go b/api/scanner/scanner_tasks/cleanup_tasks/media_cleanup_task.go new file mode 100644 index 0000000..74afb22 --- /dev/null +++ b/api/scanner/scanner_tasks/cleanup_tasks/media_cleanup_task.go @@ -0,0 +1,22 @@ +package cleanup_tasks + +import ( + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_utils" +) + +type MediaCleanupTask struct { + scanner_task.ScannerTaskBase +} + +func (t MediaCleanupTask) AfterScanAlbum(ctx scanner_task.TaskContext, changedMedia []*models.Media, + albumMedia []*models.Media) error { + + cleanupErrors := CleanupMedia(ctx.GetDB(), ctx.GetAlbum().ID, albumMedia) + for _, err := range cleanupErrors { + scanner_utils.ScannerError(ctx, "delete old media: %s", err) + } + + return nil +} diff --git a/api/scanner/scanner_tasks/exif_task.go b/api/scanner/scanner_tasks/exif_task.go new file mode 100644 index 0000000..2bf1444 --- /dev/null +++ b/api/scanner/scanner_tasks/exif_task.go @@ -0,0 +1,74 @@ +package scanner_tasks + +import ( + "errors" + "fmt" + + "gorm.io/gorm" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/externaltools/exif" + "github.com/photoview/photoview/api/scanner/scanner_task" +) + +type ExifTask struct { + scanner_task.ScannerTaskBase +} + +func (t ExifTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error { + if !newMedia { + return nil + } + + if err := SaveEXIF(ctx.GetDB(), media); err != nil { + log.Warn(ctx, "SaveEXIF failed", "title", media.Title, "error", err, "path", media.Path) + } + + return nil +} + +// SaveEXIF scans the media file for exif metadata and saves it in the database if found +func SaveEXIF(tx *gorm.DB, media *models.Media) error { + // Check if EXIF data already exists + if media.ExifID != nil { + var e models.MediaEXIF + var err error + if err = tx.First(&e, media.ExifID).Error; err == nil { + return nil + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to get EXIF for %q from database: %w", media.Path, err) + } + log.Warn( + tx.Statement.Context, + "EXIF metadata not found in database, will re-parse it", + "path", media.Path, + "error", err, + ) + media.ExifID = nil + } + + exifData, err := exif.Parse(media.Path) + if err != nil { + return fmt.Errorf("failed to parse exif data: %w", err) + } + + if exifData == nil { + return nil + } + + // Add EXIF to database and link to media + if err := tx.Model(media).Association("Exif").Replace(exifData); err != nil { + return fmt.Errorf("failed to save media exif to database: %w", err) + } + + if exifData.DateShot != nil && !exifData.DateShot.Equal(media.DateShot) { + if err := tx.Save(media).Error; err != nil { + return fmt.Errorf("failed to update EXIF metadata for the media %s: %w", media.Path, err) + } + media.DateShot = *exifData.DateShot + } + + return nil +} diff --git a/api/scanner/scanner_tasks/face_detection_task.go b/api/scanner/scanner_tasks/face_detection_task.go new file mode 100644 index 0000000..4ca8ee6 --- /dev/null +++ b/api/scanner/scanner_tasks/face_detection_task.go @@ -0,0 +1,31 @@ +package scanner_tasks + +import ( + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_utils" +) + +type FaceDetectionTask struct { + scanner_task.ScannerTaskBase +} + +func (t FaceDetectionTask) AfterProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, + updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error { + + didProcess := len(updatedURLs) > 0 + + if didProcess && mediaData.Media.Type == models.MediaTypePhoto { + media := mediaData.Media + if face_detection.GlobalFaceDetector == nil { + return nil + } + if err := face_detection.GlobalFaceDetector.DetectFaces(ctx.GetDB(), media); err != nil { + scanner_utils.ScannerError(ctx, "Error detecting faces in image (%s): %s", media.Path, err) + } + } + + return nil +} diff --git a/api/scanner/scanner_tasks/ignorefile_task.go b/api/scanner/scanner_tasks/ignorefile_task.go new file mode 100644 index 0000000..952db50 --- /dev/null +++ b/api/scanner/scanner_tasks/ignorefile_task.go @@ -0,0 +1,37 @@ +package scanner_tasks + +import ( + "io/fs" + "log" + + "github.com/photoview/photoview/api/scanner/scanner_task" + ignore "github.com/sabhiram/go-gitignore" +) + +type IgnorefileTask struct { + scanner_task.ScannerTaskBase +} + +type ignorefileTaskKey string + +const albumIgnoreKey ignorefileTaskKey = "album_ignore_key" + +func getAlbumIgnore(ctx scanner_task.TaskContext) *ignore.GitIgnore { + return ctx.Value(albumIgnoreKey).(*ignore.GitIgnore) +} + +func (t IgnorefileTask) BeforeScanAlbum(ctx scanner_task.TaskContext) (scanner_task.TaskContext, error) { + albumIgnore := ignore.CompileIgnoreLines(*ctx.GetCache().GetAlbumIgnore(ctx.GetAlbum().Path)...) + return ctx.WithValue(albumIgnoreKey, albumIgnore), nil +} + +func (t IgnorefileTask) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileInfo, mediaPath string) (bool, error) { + + // Match file against ignore data + if getAlbumIgnore(ctx).MatchesPath(fileInfo.Name()) { + log.Printf("File %s ignored\n", fileInfo.Name()) + return true, nil + } + + return false, nil +} diff --git a/api/scanner/scanner_tasks/notification_task.go b/api/scanner/scanner_tasks/notification_task.go new file mode 100644 index 0000000..da0fb6a --- /dev/null +++ b/api/scanner/scanner_tasks/notification_task.go @@ -0,0 +1,78 @@ +package scanner_tasks + +import ( + "fmt" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/notification" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/utils" +) + +type NotificationTask struct { + scanner_task.ScannerTaskBase + throttle utils.Throttle + albumKey string +} + +func NewNotificationTask() NotificationTask { + notifyThrottle := utils.NewThrottle(500 * time.Millisecond) + notifyThrottle.Trigger(nil) + + return NotificationTask{ + albumKey: utils.GenerateToken(), + throttle: notifyThrottle, + } +} + +func (t NotificationTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error { + if newMedia { + t.throttle.Trigger(func() { + notification.BroadcastNotification(&models.Notification{ + Key: t.albumKey, + Type: models.NotificationTypeMessage, + Header: fmt.Sprintf("Found new media in album '%s'", ctx.GetAlbum().Title), + Content: fmt.Sprintf("Found %s", media.Path), + }) + }) + } + + return nil +} + +func (t NotificationTask) AfterProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, + updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error { + + if len(updatedURLs) > 0 { + progress := float64(mediaIndex) / float64(mediaTotal) * 100.0 + notification.BroadcastNotification(&models.Notification{ + Key: t.albumKey, + Type: models.NotificationTypeProgress, + Header: fmt.Sprintf("Processing media for album '%s'", ctx.GetAlbum().Title), + Content: fmt.Sprintf("Processed media at %s", mediaData.Media.Path), + Progress: &progress, + }) + } + + return nil +} + +func (t NotificationTask) AfterScanAlbum(ctx scanner_task.TaskContext, changedMedia []*models.Media, + albumMedia []*models.Media) error { + + if len(changedMedia) > 0 { + timeoutDelay := 2000 + notification.BroadcastNotification(&models.Notification{ + Key: t.albumKey, + Type: models.NotificationTypeMessage, + Positive: true, + Header: fmt.Sprintf("Done processing media for album '%s'", ctx.GetAlbum().Title), + Content: "All media have been processed", + Timeout: &timeoutDelay, + }) + } + + return nil +} diff --git a/api/scanner/scanner_tasks/processing_tasks/counterpart_files_task.go b/api/scanner/scanner_tasks/processing_tasks/counterpart_files_task.go new file mode 100644 index 0000000..5fd2c23 --- /dev/null +++ b/api/scanner/scanner_tasks/processing_tasks/counterpart_files_task.go @@ -0,0 +1,62 @@ +package processing_tasks + +import ( + "fmt" + "io/fs" + + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/media_type" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/utils" +) + +type CounterpartFilesTask struct { + scanner_task.ScannerTaskBase +} + +func (t CounterpartFilesTask) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileInfo, mediaPath string) (skip bool, err error) { + fileType := media_type.GetMediaType(mediaPath) + + if !fileType.IsSupported() { + return true, nil + } + + if utils.EnvDisableRawProcessing.GetBool() { + if !fileType.IsWebCompatible() { + return true, nil + } + + // Don't skip the JPEGs if raw processing is disabled. Treat them as standalone files. + return false, nil + } + + if fileType.IsWebCompatible() { + _, existed := media_type.FindRawCounterpart(mediaPath) + if existed { + return true, nil + } + } + + return false, nil +} + +func (t CounterpartFilesTask) BeforeProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData) (scanner_task.TaskContext, error) { + + mediaType := ctx.GetCache().GetMediaType(mediaData.Media.Path) + if mediaType == media_type.TypeUnknown { + return ctx, fmt.Errorf("scan for counterpart file %s failed: media type is %s", mediaData.Media.Path, mediaType) + } + + if mediaType.IsWebCompatible() { + return ctx, nil + } + + counterpartFile, ok := media_type.FindWebCounterpart(mediaData.Media.Path) + if !ok { + return ctx, nil + } + + mediaData.CounterpartPath = &counterpartFile + + return ctx, nil +} diff --git a/api/scanner/scanner_tasks/processing_tasks/counterpart_files_task_test.go b/api/scanner/scanner_tasks/processing_tasks/counterpart_files_task_test.go new file mode 100644 index 0000000..2c584ef --- /dev/null +++ b/api/scanner/scanner_tasks/processing_tasks/counterpart_files_task_test.go @@ -0,0 +1,101 @@ +package processing_tasks + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/test_utils" + "github.com/photoview/photoview/api/utils" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} + +func TestCounterpartFilesTaskMediaFound(t *testing.T) { + mediaPath := test_utils.PathFromAPIRoot("scanner", "test_media", "real_media") + + tests := []struct { + name string + file string + disableRawProcessing bool + wantSkip bool + }{ + { + name: "StandaloneProcessRaw", + file: "standalone_jpg.jpg", + disableRawProcessing: false, + wantSkip: false, + }, + { + name: "StandaloneNotProcessRaw", + file: "standalone_jpg.jpg", + disableRawProcessing: true, + wantSkip: false, + }, + { + name: "RawJpegProcessRaw", + file: "raw_with_jpg.jpg", + disableRawProcessing: false, + wantSkip: true, + }, + { + name: "RawJpegNotProcessRaw", + file: "raw_with_jpg.jpg", + disableRawProcessing: true, + wantSkip: false, + }, + { + name: "RawProcessRaw", + file: "raw_with_jpg.tiff", + disableRawProcessing: false, + wantSkip: false, + }, + { + name: "RawNotProcessRaw", + file: "raw_with_jpg.tiff", + disableRawProcessing: true, + wantSkip: true, + }, + { + name: "UnknownProcessRaw", + file: "file.pdf", + disableRawProcessing: false, + wantSkip: true, + }, + { + name: "UnknownNotProcessRaw", + file: "file.pdf", + disableRawProcessing: true, + wantSkip: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(string(utils.EnvDisableRawProcessing), fmt.Sprintf("%v", tc.disableRawProcessing)) + + ctx := scanner_task.NewTaskContext(context.Background(), nil, nil, nil) + + fname := filepath.Join(mediaPath, tc.file) + fi, err := os.Stat(fname) + if err != nil { + t.Fatalf("Stat(%q) error: %v", fname, err) + } + + var task CounterpartFilesTask + got, err := task.MediaFound(ctx, fi, fname) + if err != nil { + t.Fatalf("task.MediaFound(ctx, %q) error: %v", fname, err) + } + + if got, want := got, tc.wantSkip; got != want { + t.Errorf("task.MediaFound(ctx, %q) = (skip)%v, want skip: %v", fname, got, want) + } + }) + } +} diff --git a/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go b/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go new file mode 100644 index 0000000..b5088d5 --- /dev/null +++ b/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go @@ -0,0 +1,126 @@ +package processing_tasks + +import ( + "os" + "path" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/pkg/errors" +) + +type ProcessPhotoTask struct { + scanner_task.ScannerTaskBase +} + +func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) ([]*models.MediaURL, error) { + if mediaData.Media.Type != models.MediaTypePhoto { + return []*models.MediaURL{}, nil + } + + updatedURLs := make([]*models.MediaURL, 0) + photo := mediaData.Media + + log.Info(ctx, "Processing photo", "photo", photo.Path) + + photoURLFromDB := makePhotoURLChecker(ctx.GetDB(), photo.ID) + + // original photo url + origURL, err := photoURLFromDB(models.MediaOriginal) + if err != nil { + return []*models.MediaURL{}, err + } + + // Thumbnail + thumbURL, err := photoURLFromDB(models.PhotoThumbnail) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "error processing photo thumbnail") + } + + // Highres + highResURL, err := photoURLFromDB(models.PhotoHighRes) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "error processing photo highres") + } + + var baseImagePath string = photo.Path + + // Generate high res jpeg + if highResURL == nil { + + contentType, err := mediaData.ContentType() + if err != nil { + return []*models.MediaURL{}, err + } + + if !contentType.IsWebCompatible() { + highresName := generateUniqueMediaNamePrefixed("highres", photo.Path, ".jpg") + baseImagePath = path.Join(mediaCachePath, highresName) + + highRes, err := generateSaveHighResJPEG(ctx.GetDB(), photo, mediaData, highresName, baseImagePath, nil) + if err != nil { + return []*models.MediaURL{}, err + } + + updatedURLs = append(updatedURLs, highRes) + } + } else { + // Verify that highres photo still exists in cache + baseImagePath = path.Join(mediaCachePath, highResURL.MediaName) + + if _, err := os.Stat(baseImagePath); os.IsNotExist(err) { + log.Info(ctx, "High-res photo found in database but not in cache, re-encoding photo to cache", "media_name", highResURL.MediaName) + updatedURLs = append(updatedURLs, highResURL) + + err = mediaData.EncodeHighRes(baseImagePath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "creating high-res cached image") + } + } + } + + // Save original photo to database + if origURL == nil { + + // Make sure photo dimensions is set + photoDimensions, err := media_encoding.GetPhotoDimensions(photo.Path) + if err != nil { + return []*models.MediaURL{}, err + } + + original, err := saveOriginalPhotoToDB(ctx.GetDB(), photo, mediaData, photoDimensions) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "saving original photo to database") + } + + updatedURLs = append(updatedURLs, original) + } + + // Save thumbnail to cache + if thumbURL == nil { + thumbnailName := generateUniqueMediaNamePrefixed("thumbnail", photo.Path, ".jpg") + thumbnail, err := generateSaveThumbnailJPEG(ctx.GetDB(), photo, thumbnailName, mediaCachePath, baseImagePath, nil) + if err != nil { + return []*models.MediaURL{}, err + } + + updatedURLs = append(updatedURLs, thumbnail) + } else { + // Verify that thumbnail photo still exists in cache + thumbPath := path.Join(mediaCachePath, thumbURL.MediaName) + + if _, err := os.Stat(thumbPath); os.IsNotExist(err) { + updatedURLs = append(updatedURLs, thumbURL) + log.Info(ctx, "Thumbnail photo found in database but not in cache, re-encoding photo to cache", "media_name", thumbURL.MediaName) + + _, err := media_encoding.EncodeThumbnail(ctx.GetDB(), baseImagePath, thumbPath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "could not create thumbnail cached image") + } + } + } + + return updatedURLs, nil +} diff --git a/api/scanner/scanner_tasks/processing_tasks/process_video_task.go b/api/scanner/scanner_tasks/processing_tasks/process_video_task.go new file mode 100644 index 0000000..6e2e1f0 --- /dev/null +++ b/api/scanner/scanner_tasks/processing_tasks/process_video_task.go @@ -0,0 +1,230 @@ +package processing_tasks + +import ( + "context" + "fmt" + "os" + "path" + "strings" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/media_encoding/executable_worker" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gopkg.in/vansante/go-ffprobe.v2" +) + +type ProcessVideoTask struct { + scanner_task.ScannerTaskBase +} + +func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) ([]*models.MediaURL, error) { + if mediaData.Media.Type != models.MediaTypeVideo { + return []*models.MediaURL{}, nil + } + + updatedURLs := make([]*models.MediaURL, 0) + video := mediaData.Media + + log.Info(ctx, "Processing video", "video", video.Path) + + mediaURLFromDB := makePhotoURLChecker(ctx.GetDB(), video.ID) + + videoOriginalURL, err := mediaURLFromDB(models.MediaOriginal) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "error processing video original format") + } + + videoWebURL, err := mediaURLFromDB(models.VideoWeb) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "error processing video web-format") + } + + videoThumbnailURL, err := mediaURLFromDB(models.VideoThumbnail) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "error processing video thumbnail") + } + + videoType, err := mediaData.ContentType() + if err != nil { + return []*models.MediaURL{}, fmt.Errorf("getting video content type error: %w", err) + } + + if videoOriginalURL == nil && videoType.IsWebCompatible() { + origVideoPath := video.Path + videoMediaName := generateUniqueMediaName(video.Path) + + webMetadata, err := ReadVideoStreamMetadata(origVideoPath) + if err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "failed to read metadata for original video (%s)", video.Title) + } + + fileStats, err := os.Stat(origVideoPath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of original video") + } + + mediaURL := models.MediaURL{ + MediaID: video.ID, + MediaName: videoMediaName, + Width: webMetadata.Width, + Height: webMetadata.Height, + Purpose: models.MediaOriginal, + ContentType: videoType.String(), + FileSize: fileStats.Size(), + } + + if err := ctx.GetDB().Create(&mediaURL).Error; err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "insert original video into database (%s)", video.Title) + } + + updatedURLs = append(updatedURLs, &mediaURL) + } + + if videoWebURL == nil && !videoType.IsWebCompatible() { + webVideoName := fmt.Sprintf("web_video_%s_%s", path.Base(video.Path), utils.GenerateToken()) + webVideoName = strings.ReplaceAll(webVideoName, ".", "_") + webVideoName = strings.ReplaceAll(webVideoName, " ", "_") + webVideoName = webVideoName + ".mp4" + + webVideoPath := path.Join(mediaCachePath, webVideoName) + + err = executable_worker.Ffmpeg.EncodeMp4(video.Path, webVideoPath) + if err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "could not encode mp4 video (%s)", video.Path) + } + + webMetadata, err := ReadVideoStreamMetadata(webVideoPath) + if err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "failed to read metadata for encoded web-video (%s)", video.Title) + } + + fileStats, err := os.Stat(webVideoPath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of web-optimized video") + } + + mediaURL := models.MediaURL{ + MediaID: video.ID, + MediaName: webVideoName, + Width: webMetadata.Width, + Height: webMetadata.Height, + Purpose: models.VideoWeb, + ContentType: "video/mp4", + FileSize: fileStats.Size(), + } + + if err := ctx.GetDB().Create(&mediaURL).Error; err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "failed to insert encoded web-video into database (%s)", video.Title) + } + + updatedURLs = append(updatedURLs, &mediaURL) + } + + probeData, err := mediaData.VideoMetadata() + if err != nil { + return []*models.MediaURL{}, err + } + + if videoThumbnailURL == nil { + videoThumbName := fmt.Sprintf("video_thumb_%s_%s", path.Base(video.Path), utils.GenerateToken()) + videoThumbName = strings.ReplaceAll(videoThumbName, ".", "_") + videoThumbName = strings.ReplaceAll(videoThumbName, " ", "_") + videoThumbName = videoThumbName + ".jpg" + + thumbImagePath := path.Join(mediaCachePath, videoThumbName) + + err = executable_worker.Ffmpeg.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData) + if err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title) + } + + thumbDimensions, err := media_encoding.GetPhotoDimensions(thumbImagePath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "get dimensions of video thumbnail image") + } + + fileStats, err := os.Stat(thumbImagePath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of video thumbnail") + } + + thumbMediaURL := models.MediaURL{ + MediaID: video.ID, + MediaName: videoThumbName, + Width: thumbDimensions.Width, + Height: thumbDimensions.Height, + Purpose: models.VideoThumbnail, + ContentType: "image/jpeg", + FileSize: fileStats.Size(), + } + + if err := ctx.GetDB().Create(&thumbMediaURL).Error; err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "failed to insert video thumbnail image into database (%s)", video.Title) + } + + updatedURLs = append(updatedURLs, &thumbMediaURL) + } else { + // Verify that video thumbnail still exists in cache + thumbImagePath := path.Join(mediaCachePath, videoThumbnailURL.MediaName) + + if _, err := os.Stat(thumbImagePath); os.IsNotExist(err) { + log.Info(ctx, "Video thumbnail found in database but not in cache, re-encoding video thumbnail to cache", "video", videoThumbnailURL.MediaName) + updatedURLs = append(updatedURLs, videoThumbnailURL) + + err = executable_worker.Ffmpeg.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData) + if err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title) + } + + thumbDimensions, err := media_encoding.GetPhotoDimensions(thumbImagePath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "get dimensions of video thumbnail image") + } + + fileStats, err := os.Stat(thumbImagePath) + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of video thumbnail") + } + + videoThumbnailURL.Width = thumbDimensions.Width + videoThumbnailURL.Height = thumbDimensions.Height + videoThumbnailURL.FileSize = fileStats.Size() + + if err := ctx.GetDB().Save(videoThumbnailURL).Error; err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "updating video thumbnail url in database after re-encoding") + } + } + } + + return updatedURLs, nil +} + +func ReadVideoMetadata(videoPath string) (*ffprobe.ProbeData, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), utils.MediaProbeTimeout()) + defer cancelFn() + + data, err := ffprobe.ProbeURL(ctx, videoPath) + if err != nil { + return nil, errors.Wrapf(err, "could not read video metadata (%s)", path.Base(videoPath)) + } + + return data, nil +} + +func ReadVideoStreamMetadata(videoPath string) (*ffprobe.Stream, error) { + data, err := ReadVideoMetadata(videoPath) + if err != nil { + return nil, errors.Wrap(err, "read video stream metadata") + } + + stream := data.FirstVideoStream() + if stream == nil { + return nil, fmt.Errorf("could not get stream from file metadata (%s)", path.Base(videoPath)) + } + + return stream, nil +} diff --git a/api/scanner/scanner_tasks/processing_tasks/processing_functions.go b/api/scanner/scanner_tasks/processing_tasks/processing_functions.go new file mode 100644 index 0000000..befa4e9 --- /dev/null +++ b/api/scanner/scanner_tasks/processing_tasks/processing_functions.go @@ -0,0 +1,97 @@ +package processing_tasks + +import ( + "os" + "path" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +func generateSaveHighResJPEG(tx *gorm.DB, media *models.Media, imageData *media_encoding.EncodeMediaData, highResName string, imagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) { + + err := imageData.EncodeHighRes(imagePath) + if err != nil { + return nil, errors.Wrap(err, "creating high-res cached image") + } + + photoDimensions, err := media_encoding.GetPhotoDimensions(imagePath) + if err != nil { + return nil, err + } + + fileStats, err := os.Stat(imagePath) + if err != nil { + return nil, errors.Wrap(err, "reading file stats of highres photo") + } + + if mediaURL == nil { + + mediaURL = &models.MediaURL{ + MediaID: media.ID, + MediaName: highResName, + Width: photoDimensions.Width, + Height: photoDimensions.Height, + Purpose: models.PhotoHighRes, + ContentType: "image/jpeg", + FileSize: fileStats.Size(), + } + + if err := tx.Create(&mediaURL).Error; err != nil { + return nil, errors.Wrapf(err, "could not insert highres media url (%d, %s)", media.ID, highResName) + } + } else { + mediaURL.Width = photoDimensions.Width + mediaURL.Height = photoDimensions.Height + mediaURL.FileSize = fileStats.Size() + + if err := tx.Save(&mediaURL).Error; err != nil { + return nil, errors.Wrapf(err, "could not update media url after side car changes (%d, %s)", media.ID, highResName) + } + } + + return mediaURL, nil +} + +func generateSaveThumbnailJPEG(tx *gorm.DB, media *models.Media, thumbnailName string, photoCachePath string, baseImagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) { + thumbOutputPath := path.Join(photoCachePath, thumbnailName) + + thumbSize, err := media_encoding.EncodeThumbnail(tx, baseImagePath, thumbOutputPath) + if err != nil { + return nil, errors.Wrap(err, "could not create thumbnail cached image") + } + + fileStats, err := os.Stat(thumbOutputPath) + if err != nil { + return nil, errors.Wrap(err, "reading file stats of thumbnail photo") + } + + if mediaURL == nil { + + mediaURL = &models.MediaURL{ + MediaID: media.ID, + MediaName: thumbnailName, + Width: thumbSize.Width, + Height: thumbSize.Height, + Purpose: models.PhotoThumbnail, + ContentType: "image/jpeg", + FileSize: fileStats.Size(), + } + + if err := tx.Create(&mediaURL).Error; err != nil { + return nil, errors.Wrapf(err, "could not insert thumbnail media url (%d, %s)", media.ID, thumbnailName) + } + } else { + mediaURL.Width = thumbSize.Width + mediaURL.Height = thumbSize.Height + mediaURL.FileSize = fileStats.Size() + + if err := tx.Save(&mediaURL).Error; err != nil { + return nil, errors.Wrapf(err, "could not update media url after side car changes (%d, %s)", media.ID, thumbnailName) + } + } + + return mediaURL, nil +} diff --git a/api/scanner/scanner_tasks/processing_tasks/processing_helpers.go b/api/scanner/scanner_tasks/processing_tasks/processing_helpers.go new file mode 100644 index 0000000..2051f6a --- /dev/null +++ b/api/scanner/scanner_tasks/processing_tasks/processing_helpers.go @@ -0,0 +1,81 @@ +package processing_tasks + +import ( + "fmt" + "os" + "path" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// Higher order function used to check if MediaURL for a given MediaPurpose exists +func makePhotoURLChecker(tx *gorm.DB, mediaID int) func(purpose models.MediaPurpose) (*models.MediaURL, error) { + return func(purpose models.MediaPurpose) (*models.MediaURL, error) { + var mediaURL []*models.MediaURL + + result := tx.Where("purpose = ?", purpose).Where("media_id = ?", mediaID).Find(&mediaURL) + + if result.Error != nil { + return nil, result.Error + } + + if result.RowsAffected > 0 { + return mediaURL[0], nil + } + + return nil, nil + } +} + +func generateUniqueMediaNamePrefixed(prefix string, mediaPath string, extension string) string { + mediaName := fmt.Sprintf("%s_%s_%s", prefix, path.Base(mediaPath), utils.GenerateToken()) + mediaName = models.SanitizeMediaName(mediaName) + mediaName = mediaName + extension + return mediaName +} + +func generateUniqueMediaName(mediaPath string) string { + + filename := path.Base(mediaPath) + baseName := filename[0 : len(filename)-len(path.Ext(filename))] + baseExt := path.Ext(filename) + + mediaName := fmt.Sprintf("%s_%s", baseName, utils.GenerateToken()) + mediaName = models.SanitizeMediaName(mediaName) + baseExt + + return mediaName +} + +func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *media_encoding.EncodeMediaData, photoDimensions media_encoding.Dimension) (*models.MediaURL, error) { + originalImageName := generateUniqueMediaName(photo.Path) + + contentType, err := imageData.ContentType() + if err != nil { + return nil, err + } + + fileStats, err := os.Stat(photo.Path) + if err != nil { + return nil, errors.Wrap(err, "reading file stats of original photo") + } + + mediaURL := models.MediaURL{ + Media: photo, + MediaName: originalImageName, + Width: photoDimensions.Width, + Height: photoDimensions.Height, + Purpose: models.MediaOriginal, + ContentType: contentType.String(), + FileSize: fileStats.Size(), + } + + if err := tx.Create(&mediaURL).Error; err != nil { + return nil, errors.Wrapf(err, "inserting original photo url: %d, %s", photo.ID, photo.Title) + } + + return &mediaURL, nil +} diff --git a/api/scanner/scanner_tasks/processing_tasks/sidecar_task.go b/api/scanner/scanner_tasks/processing_tasks/sidecar_task.go new file mode 100644 index 0000000..0c918dc --- /dev/null +++ b/api/scanner/scanner_tasks/processing_tasks/sidecar_task.go @@ -0,0 +1,160 @@ +package processing_tasks + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "log" + "os" + "path" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/media_type" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_utils" + "github.com/pkg/errors" +) + +type SidecarTask struct { + scanner_task.ScannerTaskBase +} + +func (t SidecarTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error { + if media.Type != models.MediaTypePhoto || !newMedia { + return nil + } + + mediaType := ctx.GetCache().GetMediaType(media.Path) + if mediaType == media_type.TypeUnknown { + return fmt.Errorf("scan for sidecar file %s failed: media type is %s", media.Path, mediaType) + } + + if mediaType.IsWebCompatible() { + return nil + } + + var sideCarPath *string = nil + var sideCarHash *string = nil + + sideCarPath = scanForSideCarFile(media.Path) + if sideCarPath != nil { + sideCarHash = hashSideCarFile(sideCarPath) + } + + // Add sidecar data to media + media.SideCarPath = sideCarPath + media.SideCarHash = sideCarHash + if err := ctx.GetDB().Save(media).Error; err != nil { + return errors.Wrapf(err, "update media sidecar info (%s)", *sideCarPath) + } + + return nil +} + +func (t SidecarTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) (updatedURLs []*models.MediaURL, err error) { + mediaType, err := mediaData.ContentType() + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, process media") + } + + if mediaType.IsWebCompatible() { + return []*models.MediaURL{}, nil + } + + photo := mediaData.Media + + sideCarFileHasChanged := false + var currentFileHash *string + currentSideCarPath := scanForSideCarFile(photo.Path) + + if currentSideCarPath != nil { + currentFileHash = hashSideCarFile(currentSideCarPath) + if photo.SideCarHash == nil || *photo.SideCarHash != *currentFileHash { + sideCarFileHasChanged = true + } + } else if photo.SideCarPath != nil { // sidecar has been deleted since last scan + sideCarFileHasChanged = true + } + + if !sideCarFileHasChanged { + return []*models.MediaURL{}, nil + } + + fmt.Printf("Detected changed sidecar file for %s recreating JPG's to reflect changes\n", photo.Path) + + highResURL, err := photo.GetHighRes() + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, get high-res media_url") + } + + thumbURL, err := photo.GetThumbnail() + if err != nil { + return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, get high-res media_url") + } + + // update high res image may be cropped so dimentions and file size can change + baseImagePath := path.Join(mediaCachePath, highResURL.MediaName) // update base image path for thumbnail + tempHighResPath := baseImagePath + ".hold" + os.Rename(baseImagePath, tempHighResPath) + updatedHighRes, err := generateSaveHighResJPEG(ctx.GetDB(), photo, mediaData, highResURL.MediaName, baseImagePath, highResURL) + if err != nil { + os.Rename(tempHighResPath, baseImagePath) + return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, recreating high-res cached image") + } + os.Remove(tempHighResPath) + + // update thumbnail image may be cropped so dimentions and file size can change + thumbPath := path.Join(mediaCachePath, thumbURL.MediaName) + tempThumbPath := thumbPath + ".hold" // hold onto the original image incase for some reason we fail to recreate one with the new settings + os.Rename(thumbPath, tempThumbPath) + updatedThumbnail, err := generateSaveThumbnailJPEG(ctx.GetDB(), photo, thumbURL.MediaName, mediaCachePath, baseImagePath, thumbURL) + if err != nil { + os.Rename(tempThumbPath, thumbPath) + return []*models.MediaURL{}, errors.Wrap(err, "recreating thumbnail cached image") + } + os.Remove(tempThumbPath) + + photo.SideCarHash = currentFileHash + photo.SideCarPath = currentSideCarPath + + // save new side car hash + if err := ctx.GetDB().Save(&photo).Error; err != nil { + return []*models.MediaURL{}, errors.Wrapf(err, "could not update side car hash for media: %s", photo.Path) + } + + return []*models.MediaURL{ + updatedThumbnail, + updatedHighRes, + }, nil +} + +func scanForSideCarFile(path string) *string { + testPath := path + ".xmp" + + if scanner_utils.FileExists(testPath) { + return &testPath + } + + return nil +} + +func hashSideCarFile(path *string) *string { + if path == nil { + return nil + } + + f, err := os.Open(*path) + if err != nil { + log.Printf("ERROR: %s", err) + } + defer f.Close() + + h := md5.New() + if _, err := io.Copy(h, f); err != nil { + log.Printf("ERROR: %s", err) + } + hash := hex.EncodeToString(h.Sum(nil)) + return &hash +} diff --git a/api/scanner/scanner_tasks/scanner_tasks.go b/api/scanner/scanner_tasks/scanner_tasks.go new file mode 100644 index 0000000..509aa4f --- /dev/null +++ b/api/scanner/scanner_tasks/scanner_tasks.go @@ -0,0 +1,157 @@ +package scanner_tasks + +import ( + "fmt" + "io/fs" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/scanner/media_encoding" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_tasks/cleanup_tasks" + "github.com/photoview/photoview/api/scanner/scanner_tasks/processing_tasks" +) + +var allTasks []scanner_task.ScannerTask = []scanner_task.ScannerTask{ + NotificationTask{}, + IgnorefileTask{}, + processing_tasks.CounterpartFilesTask{}, + processing_tasks.SidecarTask{}, + processing_tasks.ProcessPhotoTask{}, + processing_tasks.ProcessVideoTask{}, + FaceDetectionTask{}, + BlurhashTask{}, + ExifTask{}, + VideoMetadataTask{}, + cleanup_tasks.MediaCleanupTask{}, +} + +type scannerTasks struct { + scanner_task.ScannerTaskBase +} + +var Tasks scannerTasks = scannerTasks{} + +func simpleCombinedTasks(ctx scanner_task.TaskContext, doTask func(ctx scanner_task.TaskContext, + task scanner_task.ScannerTask) error) error { + + for _, task := range allTasks { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + err := doTask(ctx, task) + if err != nil { + return err + } + } + return nil +} + +func (t scannerTasks) BeforeScanAlbum(ctx scanner_task.TaskContext) (scanner_task.TaskContext, error) { + for _, task := range allTasks { + var err error + ctx, err = task.BeforeScanAlbum(ctx) + if err != nil { + return ctx, err + } + + select { + case <-ctx.Done(): + return ctx, ctx.Err() + default: + } + } + + return ctx, nil +} + +func (t scannerTasks) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileInfo, mediaPath string) (bool, error) { + for _, task := range allTasks { + select { + case <-ctx.Done(): + return false, ctx.Err() + default: + } + + skip, err := task.MediaFound(ctx, fileInfo, mediaPath) + + if err != nil { + return false, err + } + + if skip { + log.Info(ctx, "skip the media", "media_path", mediaPath, "by_task", fmt.Sprintf("%T", task)) + return true, nil + } + } + + return false, nil +} + +func (t scannerTasks) AfterScanAlbum(ctx scanner_task.TaskContext, changedMedia []*models.Media, + albumMedia []*models.Media) error { + + return simpleCombinedTasks(ctx, func(ctx scanner_task.TaskContext, task scanner_task.ScannerTask) error { + return task.AfterScanAlbum(ctx, changedMedia, albumMedia) + }) +} + +func (t scannerTasks) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error { + return simpleCombinedTasks(ctx, func(ctx scanner_task.TaskContext, task scanner_task.ScannerTask) error { + return task.AfterMediaFound(ctx, media, newMedia) + }) +} + +func (t scannerTasks) BeforeProcessMedia(ctx scanner_task.TaskContext, + mediaData *media_encoding.EncodeMediaData) (scanner_task.TaskContext, error) { + + for _, task := range allTasks { + select { + case <-ctx.Done(): + return ctx, ctx.Err() + default: + } + + var err error + ctx, err = task.BeforeProcessMedia(ctx, mediaData) + if err != nil { + return ctx, err + } + } + + return ctx, nil +} + +func (t scannerTasks) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, + mediaCachePath string) ([]*models.MediaURL, error) { + + allNewMedia := make([]*models.MediaURL, 0) + + for _, task := range allTasks { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + newMedia, err := task.ProcessMedia(ctx, mediaData, mediaCachePath) + if err != nil { + return []*models.MediaURL{}, err + } + + allNewMedia = append(allNewMedia, newMedia...) + } + + return allNewMedia, nil +} + +func (t scannerTasks) AfterProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, + updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error { + + return simpleCombinedTasks(ctx, func(ctx scanner_task.TaskContext, task scanner_task.ScannerTask) error { + return task.AfterProcessMedia(ctx, mediaData, updatedURLs, mediaIndex, mediaTotal) + }) +} diff --git a/api/scanner/scanner_tasks/video_metadata_task.go b/api/scanner/scanner_tasks/video_metadata_task.go new file mode 100644 index 0000000..9665328 --- /dev/null +++ b/api/scanner/scanner_tasks/video_metadata_task.go @@ -0,0 +1,100 @@ +package scanner_tasks + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_tasks/processing_tasks" + "github.com/pkg/errors" + "gopkg.in/vansante/go-ffprobe.v2" + "gorm.io/gorm" +) + +type VideoMetadataTask struct { + scanner_task.ScannerTaskBase +} + +func (t VideoMetadataTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error { + + if !newMedia || media.Type != models.MediaTypeVideo { + return nil + } + + err := ScanVideoMetadata(ctx.GetDB(), media) + if err != nil { + log.Printf("WARN: ScanVideoMetadata for %s failed: %s\n", media.Title, err) + } + + return nil +} + +func ScanVideoMetadata(tx *gorm.DB, video *models.Media) error { + + data, err := processing_tasks.ReadVideoMetadata(video.Path) + if err != nil { + return errors.Wrapf(err, "scan video metadata failed (%s)", video.Title) + } + + stream := data.FirstVideoStream() + if stream == nil { + return errors.New(fmt.Sprintf("could not get video stream from metadata (%s)", video.Path)) + } + + audio := data.FirstAudioStream() + var audioText string + if audio == nil { + audioText = "No audio" + } else { + switch audio.Channels { + case 0: + audioText = "No audio" + case 1: + audioText = "Mono audio" + case 2: + audioText = "Stereo audio" + default: + audioText = fmt.Sprintf("Audio (%d channels)", audio.Channels) + } + } + + framerate := getFrameRate(stream) + + videoMetadata := models.VideoMetadata{ + Width: stream.Width, + Height: stream.Height, + Duration: data.Format.DurationSeconds, + Codec: &stream.CodecLongName, + Framerate: framerate, + Bitrate: &stream.BitRate, + ColorProfile: &stream.Profile, + Audio: &audioText, + } + + video.VideoMetadata = &videoMetadata + + if err := tx.Save(video).Error; err != nil { + return errors.Wrapf(err, "failed to add video metadata to database (%s)", video.Title) + } + + return nil +} + +func getFrameRate(stream *ffprobe.Stream) *float64 { + var framerate *float64 = nil + if stream.AvgFrameRate != "" { + parts := strings.Split(stream.AvgFrameRate, "/") + if len(parts) == 2 { + if numerator, err := strconv.ParseInt(parts[0], 10, 64); err == nil { + if denominator, err := strconv.ParseInt(parts[1], 10, 64); err == nil { + result := float64(numerator) / float64(denominator) + framerate = &result + } + } + } + } + return framerate +} diff --git a/api/scanner/scanner_test.go b/api/scanner/scanner_test.go new file mode 100644 index 0000000..36748af --- /dev/null +++ b/api/scanner/scanner_test.go @@ -0,0 +1,303 @@ +package scanner_test + +import ( + "fmt" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/face_detection" + "github.com/photoview/photoview/api/test_utils" + scanner_utils "github.com/photoview/photoview/api/test_utils/scanner" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} + +func TestFullScan(t *testing.T) { + test_utils.FilesystemTest(t) + db := test_utils.DatabaseTest(t) + + pass := "1234" + user, err := models.RegisterUser(db, "test_user", &pass, true) + if err != nil { + t.Fatal("register user error:", err) + } + + rootAlbum := models.Album{ + Title: "root album", + Path: "./test_media", + } + + wantWebPhotos := []string{ + "bmp.bmp", + "gif.gif", + "jpeg.jpg", + "png.png", + "webp.webp", + + "jpg_with_file.jpg", + "recoverable_bad_rst_marker.jpg", + "standalone_jpg.jpg", + + "boy1.jpg", + "boy2.jpg", + "buttercup_close_summer_yellow.jpg", + "girl_black_hair2.jpg", + "girl_blond1.jpg", + "girl_blond2.jpg", + "girl_blond3.jpg", + "lilac_lilac_bush_lilac.jpg", + "mount_merapi_volcano_indonesia.jpg", + + "left_arrow_normal_web.jpg", + "up_arrow_90cw_web.jpg", + } + wantNonWebPhotos := []string{ + "heif.heif", + "jpegxl.jxl", + "jpg2000.jp2", + "raw_with_file.tiff", + "raw_with_jpg.tiff", + "standalone_raw.tiff", + "tiff.tiff", + "cr3.cr3", + + "left_arrow_normal_nonweb.tiff", + "up_arrow_90cw_nonweb.tiff", + } + wantWebVideos := []string{ + "mp4.mp4", + "mpeg.mpg", + "ogg.ogg", + "webm.webm", + } + wantNonWebVideos := []string{ + "avi.avi", + "mkv.mkv", + "quicktime.mov", + "wmv.wmv", + } + + wantFaceGroups := [][]string{ + {"boy1.jpg", "boy2.jpg"}, + {"girl_black_hair2.jpg"}, + {"girl_blond1.jpg", "girl_blond2.jpg", "girl_blond3.jpg"}, + } + + for i := range wantFaceGroups { + slices.Sort(wantFaceGroups[i]) + } + slices.SortFunc(wantFaceGroups, func(a, b []string) int { + return strings.Compare(fmt.Sprint(a), fmt.Sprint(b)) + }) + + if err := db.Save(&rootAlbum).Error; err != nil { + t.Fatal("create root album error:", err) + } + + if err := db.Model(user).Association("Albums").Append(&rootAlbum); err != nil { + t.Fatal("bind root album error:", err) + } + + if err := face_detection.InitializeFaceDetector(db); err != nil { + t.Fatal("initalize face detector error:", err) + } + + scanner_utils.RunScannerOnUser(t, db, user) + + t.Run("CheckMedia", func(t *testing.T) { + var allMedia []*models.Media + if err := db.Find(&allMedia).Error; err != nil { + t.Fatal("get all media error:", err) + } + + want := []string{} + want = append(want, wantWebPhotos...) + want = append(want, wantNonWebPhotos...) + want = append(want, wantWebVideos...) + want = append(want, wantNonWebVideos...) + slices.Sort(want) + + got := make([]string, len(allMedia)) + for i, media := range allMedia { + got[i] = media.Title + + if media.Blurhash == nil { + t.Errorf("media %q(%s) doesn't have Blurhash, while it should have", media.Title, media.Type) + } + + if media.Date().IsZero() { + t.Errorf("media %q(%s) doesn't have Date, while it should have", media.Title, media.Type) + } + } + slices.Sort(got) + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("all media diff (-got, +want):\n%s", diff) + } + }) + + t.Run("CheckMediaURL", func(t *testing.T) { + var allMediaURL []*models.MediaURL + if err := db.Find(&allMediaURL).Error; err != nil { + t.Fatal("get all media url error:", err) + } + + var want []string + want = append(want, wantWebPhotos...) + for _, name := range wantWebPhotos { + want = append(want, "thumbnail_"+strings.ReplaceAll(name, ".", "_")+".jpg") + } + + want = append(want, wantNonWebPhotos...) + for _, name := range wantNonWebPhotos { + want = append(want, "thumbnail_"+strings.ReplaceAll(name, ".", "_")+".jpg") + want = append(want, "highres_"+strings.ReplaceAll(name, ".", "_")+".jpg") + } + + want = append(want, wantWebVideos...) + for _, name := range wantWebVideos { + want = append(want, "video_thumb_"+strings.ReplaceAll(name, ".", "_")+".jpg") + } + + for _, name := range wantNonWebVideos { + want = append(want, "video_thumb_"+strings.ReplaceAll(name, ".", "_")+".jpg") + want = append(want, "web_video_"+strings.ReplaceAll(name, ".", "_")+".mp4") + } + + slices.Sort(want) + + if got, want := len(allMediaURL), len(want); got != want { + t.Errorf("got = %d, want: %v", got, want) + } + + got := make([]string, len(allMediaURL)) + for i, media := range allMediaURL { + got[i] = media.MediaName + } + slices.Sort(got) + + if diff := cmp.Diff(got, want, cmp.Comparer(equalNameWithoutSuffix)); diff != "" { + t.Errorf("all media url diff (-got, +want):\n%s", diff) + } + }) + + t.Run("CheckFaceGroup", func(t *testing.T) { + var allFaceGroups []*models.FaceGroup + if err := db.Find(&allFaceGroups).Error; err != nil { + t.Fatal("get face groups error:", err) + } + + if got, want := len(allFaceGroups), len(wantFaceGroups); got != want { + t.Errorf("len(allFaceGroups) = %d, want: %d", got, want) + } + }) + + t.Run("CheckFaces", func(t *testing.T) { + var allImageFaces []*models.ImageFace + if err := db.Find(&allImageFaces).Error; err != nil { + t.Fatal("get face images error:", err) + } + + for _, face := range allImageFaces { + if err := face.FillMedia(db); err != nil { + t.Fatalf("fill media for face %v error: %v", face, err) + } + } + + got := groupMediaWithFaces(allImageFaces) + + if diff := cmp.Diff(got, wantFaceGroups); diff != "" { + t.Errorf("all media diff (-got, +want):\n%s", diff) + } + }) + + t.Run("CheckPhotosOrientation", func(t *testing.T) { + photoFiles := []string{ + "left_arrow_normal_web.jpg", + "up_arrow_90cw_web.jpg", + "left_arrow_normal_nonweb.tiff", + "up_arrow_90cw_nonweb.tiff", + } + for _, filename := range photoFiles { + var media models.Media + if err := db.Preload("MediaURL").Where("title = ?", filename).Find(&media).Error; err != nil { + t.Fatalf("can't find media with name %q: %v", filename, err) + } + + thumbnail, err := media.GetThumbnail() + if err != nil { + t.Fatalf("can't get thumbnail of media %q: %v", filename, err) + } + + switch { + case strings.HasPrefix(filename, "up"): + if thumbnail.Width >= thumbnail.Height { + t.Errorf("media %q dimension: %dx%d, which should be a vertial photo", filename, thumbnail.Width, thumbnail.Height) + } + case strings.HasPrefix(filename, "left"): + if thumbnail.Width <= thumbnail.Height { + t.Errorf("media %q dimension: %dx%d, which should be a horizontal photo", filename, thumbnail.Width, thumbnail.Height) + } + } + } + }) +} + +func equalNameWithoutSuffix(a, b string) bool { + extA := filepath.Ext(a) + mainA := strings.TrimSuffix(a, extA) + extB := filepath.Ext(b) + mainB := strings.TrimSuffix(b, extB) + + // ext names are not same + if extA != extB { + return false + } + + // a is not prefix of b and b is not prefix of a + if strings.HasPrefix(mainA, mainB) && strings.HasPrefix(mainB, mainA) { + return false + } + + return true +} + +func groupMediaWithFaces(medias []*models.ImageFace) [][]string { + grouped := make(map[int][]string) + + for _, media := range medias { + group := grouped[media.FaceGroupID] + group = append(group, media.Media.Title) + grouped[media.FaceGroupID] = group + } + + ret := make([][]string, 0, len(grouped)) + for _, medias := range grouped { + slices.Sort(medias) + ret = append(ret, medias) + } + + slices.SortFunc(ret, func(a, b []string) int { + return strings.Compare(fmt.Sprint(a), fmt.Sprint(b)) + }) + + return ret +} + +func copyFilelistWithJpgExt(list []string) []string { + ret := make([]string, 0, len(list)) + for _, f := range list { + ext := filepath.Ext(f) + main := strings.TrimSuffix(f, ext) + + ret = append(ret, main+".jpg") + } + + return ret +} diff --git a/api/scanner/scanner_user.go b/api/scanner/scanner_user.go new file mode 100644 index 0000000..9a3aa4f --- /dev/null +++ b/api/scanner/scanner_user.go @@ -0,0 +1,292 @@ +package scanner + +import ( + "bufio" + "container/list" + "log" + "os" + "path" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/scanner_cache" + "github.com/photoview/photoview/api/scanner/scanner_tasks/cleanup_tasks" + "github.com/photoview/photoview/api/scanner/scanner_utils" + "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" + ignore "github.com/sabhiram/go-gitignore" + "gorm.io/gorm" +) + +func getPhotoviewIgnore(ignorePath string) ([]string, error) { + var photoviewIgnore []string + + // Open .photoviewignore file, if exists + photoviewIgnoreFile, err := os.Open(path.Join(ignorePath, ".photoviewignore")) + if err != nil { + if os.IsNotExist(err) { + return photoviewIgnore, nil + } + return photoviewIgnore, err + } + + // Close file on exit + defer photoviewIgnoreFile.Close() + + // Read and save .photoviewignore data + scanner := bufio.NewScanner(photoviewIgnoreFile) + for scanner.Scan() { + photoviewIgnore = append(photoviewIgnore, scanner.Text()) + log.Printf("Ignore found: %s", scanner.Text()) + } + + return photoviewIgnore, scanner.Err() +} + +func FindAlbumsForUser(db *gorm.DB, user *models.User, albumCache *scanner_cache.AlbumScannerCache) ([]*models.Album, []error) { + + if err := user.FillAlbums(db); err != nil { + return nil, []error{err} + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + var userRootAlbums []*models.Album + if err := db. + Where("id IN (?)", userAlbumIDs). + Where("parent_album_id IS NULL OR parent_album_id NOT IN (?)", userAlbumIDs). + Order("path ASC"). + Find(&userRootAlbums).Error; err != nil { + return nil, []error{err} + } + + scanErrors := make([]error, 0) + + type scanInfo struct { + path string + parent *models.Album + ignore []string + } + + scanQueue := list.New() + + for _, album := range userRootAlbums { + // Check if user album directory exists on the file system + if _, err := os.Stat(album.Path); err != nil { + if os.IsNotExist(err) { + scanErrors = append(scanErrors, errors.Errorf("Album directory for user '%s' does not exist '%s'\n", user.Username, album.Path)) + } else { + scanErrors = append(scanErrors, errors.Errorf("Could not read album directory for user '%s': %s\n", user.Username, album.Path)) + } + } else { + scanQueue.PushBack(scanInfo{ + path: album.Path, + parent: nil, + ignore: nil, + }) + } + } + + userAlbums := make([]*models.Album, 0) + + for scanQueue.Front() != nil { + albumInfo := scanQueue.Front().Value.(scanInfo) + scanQueue.Remove(scanQueue.Front()) + + albumPath := albumInfo.path + albumParent := albumInfo.parent + albumIgnore := albumInfo.ignore + + // Read path + dirContent, err := os.ReadDir(albumPath) + if err != nil { + scanErrors = append(scanErrors, errors.Wrapf(err, "read directory (%s)", albumPath)) + continue + } + + // Skip this dir if in ignore list + ignorePaths := ignore.CompileIgnoreLines(albumIgnore...) + if ignorePaths.MatchesPath(albumPath + "/") { + log.Printf("Skip, directroy %s is in ignore file", albumPath) + continue + } + + // Update ignore dir list + photoviewIgnore, err := getPhotoviewIgnore(albumPath) + if err != nil { + log.Printf("Failed to get ignore file, err = %s", err) + } else { + albumIgnore = append(albumIgnore, photoviewIgnore...) + } + + // Will become new album or album from db + var album *models.Album + + transErr := db.Transaction(func(tx *gorm.DB) error { + log.Printf("Scanning directory: %s", albumPath) + + // check if album already exists + var albumResult []models.Album + result := tx.Where("path_hash = ?", models.MD5Hash(albumPath)).Find(&albumResult) + if result.Error != nil { + return result.Error + } + + // album does not exist, create new + if len(albumResult) == 0 { + albumTitle := path.Base(albumPath) + + var albumParentID *int + parentOwners := make([]models.User, 0) + if albumParent != nil { + albumParentID = &albumParent.ID + + if err := tx.Model(&albumParent).Association("Owners").Find(&parentOwners); err != nil { + return err + } + } + + album = &models.Album{ + Title: albumTitle, + ParentAlbumID: albumParentID, + Path: albumPath, + } + + // Store album ignore + albumCache.InsertAlbumIgnore(albumPath, albumIgnore) + + if err := tx.Create(&album).Error; err != nil { + return errors.Wrap(err, "insert album into database") + } + + if err := tx.Model(&album).Association("Owners").Append(parentOwners); err != nil { + return errors.Wrap(err, "add owners to album") + } + } else { + album = &albumResult[0] + + // Add user as an owner of the album if not already + var userAlbumOwner []models.User + if err := tx.Model(&album).Association("Owners").Find(&userAlbumOwner, "user_albums.user_id = ?", user.ID); err != nil { + return err + } + if len(userAlbumOwner) == 0 { + newUser := models.User{} + newUser.ID = user.ID + if err := tx.Model(&album).Association("Owners").Append(&newUser); err != nil { + return err + } + } + + // Update album ignore + albumCache.InsertAlbumIgnore(albumPath, albumIgnore) + } + + userAlbums = append(userAlbums, album) + + return nil + }) + + if transErr != nil { + scanErrors = append(scanErrors, errors.Wrap(transErr, "begin database transaction")) + continue + } + + // Scan for sub-albums + for _, item := range dirContent { + subalbumPath := path.Join(albumPath, item.Name()) + + // Skip if directory is hidden + if path.Base(subalbumPath)[0:1] == "." { + continue + } + + isDirSymlink, err := utils.IsDirSymlink(subalbumPath) + if err != nil { + scanErrors = append(scanErrors, errors.Wrapf(err, "could not check for symlink target of %s", subalbumPath)) + continue + } + + if (item.IsDir() || isDirSymlink) && directoryContainsPhotos(subalbumPath, albumCache, albumIgnore) { + scanQueue.PushBack(scanInfo{ + path: subalbumPath, + parent: album, + ignore: albumIgnore, + }) + } + } + } + + deleteErrors := cleanup_tasks.DeleteOldUserAlbums(db, userAlbums, user) + scanErrors = append(scanErrors, deleteErrors...) + + return userAlbums, scanErrors +} + +func directoryContainsPhotos(rootPath string, cache *scanner_cache.AlbumScannerCache, albumIgnore []string) bool { + + if containsImage := cache.AlbumContainsPhotos(rootPath); containsImage != nil { + return *containsImage + } + + scanQueue := list.New() + scanQueue.PushBack(rootPath) + + scannedDirectories := make([]string, 0) + + for scanQueue.Front() != nil { + + dirPath := scanQueue.Front().Value.(string) + scanQueue.Remove(scanQueue.Front()) + + scannedDirectories = append(scannedDirectories, dirPath) + + // Update ignore dir list + photoviewIgnore, err := getPhotoviewIgnore(dirPath) + if err != nil { + log.Printf("Failed to get ignore file, err = %s", err) + } else { + albumIgnore = append(albumIgnore, photoviewIgnore...) + } + ignoreEntries := ignore.CompileIgnoreLines(albumIgnore...) + + dirContent, err := os.ReadDir(dirPath) + if err != nil { + scanner_utils.ScannerError(nil, "Could not read directory (%s): %s\n", dirPath, err.Error()) + return false + } + + for _, fileInfo := range dirContent { + filePath := path.Join(dirPath, fileInfo.Name()) + + isDirSymlink, err := utils.IsDirSymlink(filePath) + if err != nil { + log.Printf("Cannot detect whether %s is symlink to a directory. Pretending it is not", filePath) + isDirSymlink = false + } + + if fileInfo.IsDir() || isDirSymlink { + scanQueue.PushBack(filePath) + } else { + if cache.IsPathMedia(filePath) { + if ignoreEntries.MatchesPath(fileInfo.Name()) { + log.Printf("Match found %s, continue search for media", fileInfo.Name()) + continue + } + log.Printf("Insert Album %s %s, contains photo is true", dirPath, rootPath) + cache.InsertAlbumPaths(dirPath, rootPath, true) + return true + } + } + } + + } + + for _, scanned_path := range scannedDirectories { + log.Printf("Insert Album %s, contains photo is false", scanned_path) + cache.InsertAlbumPath(scanned_path, false) + } + return false +} diff --git a/api/scanner/scanner_utils/file_utils.go b/api/scanner/scanner_utils/file_utils.go new file mode 100644 index 0000000..68e8213 --- /dev/null +++ b/api/scanner/scanner_utils/file_utils.go @@ -0,0 +1,19 @@ +package scanner_utils + +import ( + "log" + "os" +) + +func FileExists(testPath string) bool { + _, err := os.Stat(testPath) + + if os.IsNotExist(err) { + return false + } else if err != nil { + // unexpected error logging + log.Printf("Error: checking for file existence (%s): %s", testPath, err) + return false + } + return true +} diff --git a/api/scanner/scanner_utils/scanner_error.go b/api/scanner/scanner_utils/scanner_error.go new file mode 100644 index 0000000..196ba4a --- /dev/null +++ b/api/scanner/scanner_utils/scanner_error.go @@ -0,0 +1,24 @@ +package scanner_utils + +import ( + "context" + "fmt" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/notification" + "github.com/photoview/photoview/api/log" + "github.com/photoview/photoview/api/utils" +) + +func ScannerError(ctx context.Context, format string, args ...any) { + message := fmt.Sprintf(format, args...) + + log.Error(ctx, message) + notification.BroadcastNotification(&models.Notification{ + Key: utils.GenerateToken(), + Type: models.NotificationTypeMessage, + Header: "Scanner error", + Content: message, + Negative: true, + }) +} diff --git a/api/server.go b/api/server.go new file mode 100644 index 0000000..0304466 --- /dev/null +++ b/api/server.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "path" + "syscall" + "time" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + + "github.com/joho/godotenv" + + "github.com/photoview/photoview/api/database" + "github.com/photoview/photoview/api/dataloader" + "github.com/photoview/photoview/api/graphql/auth" + graphql_endpoint "github.com/photoview/photoview/api/graphql/endpoint" + "github.com/photoview/photoview/api/routes" + "github.com/photoview/photoview/api/scanner/externaltools/exif" + "github.com/photoview/photoview/api/scanner/face_detection" + "github.com/photoview/photoview/api/scanner/media_encoding/executable_worker" + "github.com/photoview/photoview/api/scanner/periodic_scanner" + "github.com/photoview/photoview/api/scanner/scanner_queue" + "github.com/photoview/photoview/api/server" + "github.com/photoview/photoview/api/utils" + + "github.com/99designs/gqlgen/graphql/playground" +) + +func main() { + log.Println("Starting Photoview...") + + if err := godotenv.Load(); err != nil { + log.Println("No .env file found. If Photoview runs in Docker, this is expected and correct.") + } + + terminateWorkers := executable_worker.Initialize() + defer terminateWorkers() + + devMode := utils.DevelopmentMode() + + db, err := database.SetupDatabase() + if err != nil { + log.Panicf("Could not connect to database: %s\n", err) + } + + // Migrate database + if err := database.MigrateDatabase(db); err != nil { + log.Panicf("Could not migrate database: %s\n", err) + } + + exifCleanup, err := exif.Initialize() + if err != nil { + log.Panicf("Could not initialize exif parser: %s", err) + } + defer exifCleanup() + + if err := scanner_queue.InitializeScannerQueue(db); err != nil { + log.Panicf("Could not initialize scanner queue: %s\n", err) + } + + if err := periodic_scanner.InitializePeriodicScanner(db); err != nil { + log.Panicf("Could not initialize periodic scanner: %s", err) + } + + if err := face_detection.InitializeFaceDetector(db); err != nil { + log.Panicf("Could not initialize face detector: %s\n", err) + } + + rootRouter := mux.NewRouter() + rootRouter.Use(dataloader.Middleware(db)) + rootRouter.Use(auth.Middleware(db)) + rootRouter.Use(server.LoggingMiddleware) + rootRouter.Use(server.CORSMiddleware(devMode)) + + apiListenURL := utils.ApiListenUrl() + + endpointRouter := rootRouter.PathPrefix(apiListenURL.Path).Subrouter() + + if devMode { + endpointRouter.Handle("/", playground.Handler("GraphQL playground", path.Join(apiListenURL.Path, "graphql"))) + } else { + endpointRouter.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("photoview api endpoint")) + }) + } + + endpointRouter.Handle("/graphql", handlers.CompressHandler(graphql_endpoint.GraphqlEndpoint(db))) + + photoRouter := endpointRouter.PathPrefix("/photo").Subrouter() + routes.RegisterPhotoRoutes(db, photoRouter) + + videoRouter := endpointRouter.PathPrefix("/video").Subrouter() + routes.RegisterVideoRoutes(db, videoRouter) + + downloadsRouter := endpointRouter.PathPrefix("/download").Subrouter() + routes.RegisterDownloadRoutes(db, downloadsRouter) + + shouldServeUI := utils.ShouldServeUI() + + if shouldServeUI { + spa, err := routes.NewSpaHandler(utils.UIPath(), "index.html") + if err != nil { + log.Panicf("Could not configure UI handler: %s", err) + } + rootRouter.PathPrefix("/").Handler(spa) + } + + if devMode { + log.Printf("🚀 Graphql playground ready at %s\n", apiListenURL.String()) + } else { + log.Printf("Photoview API endpoint listening at %s\n", apiListenURL.String()) + + apiEndpoint := utils.ApiEndpointUrl() + log.Printf("Photoview API public endpoint ready at %s\n", apiEndpoint.String()) + + logUIendpointURL() + + if !shouldServeUI { + log.Printf("Notice: UI is not served by the API (%s=0)", utils.EnvServeUI.GetName()) + } + } + + srv := &http.Server{ + Addr: apiListenURL.Host, + Handler: rootRouter, + } + + setupGracefulShutdown(srv) + + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Panicf("HTTP server failed: %s", err) + } +} + +func setupGracefulShutdown(svr *http.Server) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + log.Println("Shutting down Photoview...") + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) // Wait for 1m to shutdown + defer cancel() + + // Shutdown scanners in correct order + periodic_scanner.ShutdownPeriodicScanner() + scanner_queue.CloseScannerQueue() + + if err := svr.Shutdown(ctx); err != nil { + log.Printf("Server shutdown error: %s", err) + } else { + log.Println("Shutdown complete") + } + }() +} + +func logUIendpointURL() { + if uiEndpoint := utils.UiEndpointUrl(); uiEndpoint != nil { + log.Printf("Photoview UI public endpoint ready at %s\n", uiEndpoint.String()) + } else { + log.Println("Photoview UI public endpoint ready at /") + } +} diff --git a/api/server/cors_middleware.go b/api/server/cors_middleware.go new file mode 100644 index 0000000..acbf39c --- /dev/null +++ b/api/server/cors_middleware.go @@ -0,0 +1,55 @@ +package server + +import ( + "net/http" + "net/url" + "strings" + + "github.com/gorilla/mux" + "github.com/photoview/photoview/api/utils" +) + +func CORSMiddleware(devMode bool) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + + var uiEndpoint *url.URL = nil + + if devMode { + // Development environment + w.Header().Set("Access-Control-Allow-Origin", req.Header.Get("origin")) + w.Header().Set("Vary", "Origin") + } else { + // Production environment + uiEndpoint = utils.UiEndpointUrl() + if uiEndpoint != nil { + // Only allow CORS if UI endpoint is defined + w.Header().Set("Access-Control-Allow-Origin", uiEndpoint.Scheme+"://"+uiEndpoint.Host) + } + } + + w = handleCORS(devMode, uiEndpoint, w) + + if req.Method != http.MethodOptions { + next.ServeHTTP(w, req) + } else { + w.WriteHeader(200) + } + }) + } +} + +func handleCORS(devMode bool, uiEndpoint *url.URL, w http.ResponseWriter) http.ResponseWriter { + corsEnabled := devMode || uiEndpoint != nil + if corsEnabled { + methods := []string{http.MethodGet, http.MethodPost, http.MethodOptions} + requestHeaders := []string{"authorization", "content-type", "content-length", "TokenPassword"} + responseHeaders := []string{"content-length"} + + w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ", ")) + w.Header().Set("Access-Control-Allow-Headers", strings.Join(requestHeaders, ", ")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Expose-Headers", strings.Join(responseHeaders, ", ")) + } + return w +} diff --git a/api/server/logging.go b/api/server/logging.go new file mode 100644 index 0000000..c56e827 --- /dev/null +++ b/api/server/logging.go @@ -0,0 +1,98 @@ +package server + +import ( + "bufio" + "errors" + "fmt" + "net" + "net/http" + "time" + + "github.com/photoview/photoview/api/graphql/auth" + "github.com/wsxiaoys/terminal/color" +) + +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + statusWriter := newStatusResponseWriter(&w) + next.ServeHTTP(statusWriter, r) + + elapsed := time.Since(start) + date := time.Now().Format("2006/01/02 15:04:05") + + status := statusWriter.status + var statusColor string + switch { + case status < 200: + statusColor = color.Colorize("b") + case status < 300: + statusColor = color.Colorize("g") + case status < 400: + statusColor = color.Colorize("c") + case status < 500: + statusColor = color.Colorize("y") + default: + statusColor = color.Colorize("r") + } + + method := r.Method + var methodColor string + switch { + case method == http.MethodGet: + methodColor = color.Colorize("b") + case method == http.MethodPost: + methodColor = color.Colorize("g") + case method == http.MethodOptions: + methodColor = color.Colorize("y") + default: + methodColor = color.Colorize("r") + } + + user := auth.UserFromContext(r.Context()) + userText := "unauthenticated" + if user != nil { + userText = color.Sprintf("@ruser: %s", user.Username) + } + + statusText := color.Sprintf("%s%s %s%d", methodColor, r.Method, statusColor, status) + requestText := fmt.Sprintf("%s%s", r.Host, r.URL.Path) + durationText := color.Sprintf("@c%s", elapsed) + + fmt.Printf("%s %s %s %s %s\n", date, statusText, requestText, durationText, userText) + + }) +} + +type statusResponseWriter struct { + http.ResponseWriter + status int + hijacker http.Hijacker +} + +func newStatusResponseWriter(w *http.ResponseWriter) *statusResponseWriter { + return &statusResponseWriter{ + ResponseWriter: *w, + hijacker: (*w).(http.Hijacker), + } +} + +func (w *statusResponseWriter) WriteHeader(status int) { + w.status = status + w.ResponseWriter.WriteHeader(status) +} + +func (w *statusResponseWriter) Write(b []byte) (int, error) { + if w.status == 0 { + w.status = 200 + } + return w.ResponseWriter.Write(b) +} + +func (w *statusResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if w.hijacker == nil { + return nil, nil, errors.New("http.Hijacker not implemented by underlying http.ResponseWriter") + } + return w.hijacker.Hijack() +} diff --git a/api/server/websocket.go b/api/server/websocket.go new file mode 100644 index 0000000..e627db6 --- /dev/null +++ b/api/server/websocket.go @@ -0,0 +1,47 @@ +package server + +import ( + "log" + "net/http" + "net/url" + + "github.com/gorilla/websocket" + "github.com/photoview/photoview/api/utils" +) + +func WebsocketUpgrader(devMode bool) websocket.Upgrader { + return websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + if devMode { + return true + } else { + uiEndpoint := utils.UiEndpointUrl() + if uiEndpoint == nil { + return true + } + + if r.Header.Get("origin") == "" { + return true + } + + originURL, err := url.Parse(r.Header.Get("origin")) + if err != nil { + log.Printf("Could not parse origin header of websocket request: %s", err) + return false + } + + return isUIOnSameHost(uiEndpoint, originURL) + } + }, + } +} + +func isUIOnSameHost(uiEndpoint *url.URL, originURL *url.URL) bool { + if uiEndpoint.Host == originURL.Host { + return true + } else { + log.Printf("Not allowing websocket request from %s because it doesn't match PHOTOVIEW_UI_ENDPOINT %s", + originURL.Host, uiEndpoint.Host) + return false + } +} diff --git a/api/test_utils/env.go b/api/test_utils/env.go new file mode 100644 index 0000000..4f7bef1 --- /dev/null +++ b/api/test_utils/env.go @@ -0,0 +1,21 @@ +package test_utils + +import ( + "path/filepath" + "runtime" +) + +// PathFromAPIRoot returns the real path in the API project root. +func PathFromAPIRoot(rootRelatedPaths ...string) string { + _, file, _, ok := runtime.Caller(0) + if !ok { + panic("Can't get the path of current function. It should not happen.") + } + + base := filepath.Dir(file) + args := make([]string, 0, len(rootRelatedPaths)+2) + args = append(args, []string{base, ".."}...) + args = append(args, rootRelatedPaths...) + + return filepath.Join(args...) +} diff --git a/api/test_utils/env_test.go b/api/test_utils/env_test.go new file mode 100644 index 0000000..c409945 --- /dev/null +++ b/api/test_utils/env_test.go @@ -0,0 +1,23 @@ +package test_utils + +import ( + "strings" + "testing" +) + +func TestPathFromAPIRoot(t *testing.T) { + tests := []struct { + paths []string + want string + }{ + {[]string{"server.go"}, "/api/server.go"}, + {[]string{"scanner", "..", "server.go"}, "/api/server.go"}, + {[]string{"scanner", "scanner_test.go"}, "/api/scanner/scanner_test.go"}, + } + + for _, tc := range tests { + if got, want := PathFromAPIRoot(tc.paths...), tc.want; !strings.HasSuffix(got, want) { + t.Fatalf("PathFromAPIRoot(%v) = %q, want a suffix: %q", tc.paths, got, want) + } + } +} diff --git a/api/test_utils/flags/flags.go b/api/test_utils/flags/flags.go new file mode 100644 index 0000000..4b3fc8c --- /dev/null +++ b/api/test_utils/flags/flags.go @@ -0,0 +1,16 @@ +package flags + +import "flag" + +var ( + // Database enables database-integration tests when the `-database` flag is passed to `go test`. + Database bool + + // Filesystem enables filesystem-integration tests when the `-filesystem` flag is passed to `go test`. + Filesystem bool +) + +func init() { + flag.BoolVar(&Database, "database", false, "run database integration tests") + flag.BoolVar(&Filesystem, "filesystem", false, "run filesystem integration tests") +} diff --git a/api/test_utils/integration_setup.go b/api/test_utils/integration_setup.go new file mode 100644 index 0000000..82699ee --- /dev/null +++ b/api/test_utils/integration_setup.go @@ -0,0 +1,79 @@ +package test_utils + +import ( + "flag" + "log" + "os" + "testing" + + "github.com/joho/godotenv" + "github.com/photoview/photoview/api/scanner/externaltools/exif" + "github.com/photoview/photoview/api/scanner/media_encoding/executable_worker" + "github.com/photoview/photoview/api/test_utils/flags" + "github.com/photoview/photoview/api/utils" + "gorm.io/gorm" +) + +var test_dbm TestDBManager = TestDBManager{} + +func UnitTestRun(m *testing.M) { + exitCode := 1 + defer func() { + os.Exit(exitCode) + }() + + flag.Parse() + + exitCode = m.Run() +} + +func IntegrationTestRun(m *testing.M) { + exitCode := 1 + defer func() { + os.Exit(exitCode) + }() + + flag.Parse() + + if flags.Database { + envPath := PathFromAPIRoot("testing.env") + + if err := godotenv.Load(envPath); err != nil { + log.Println("No testing.env file found") + } + } + defer test_dbm.Close() + + faceModelsPath := PathFromAPIRoot("data", "models") + utils.ConfigureTestFaceRecognitionModelsPath(faceModelsPath) + + exifCleanup, err := exif.Initialize() + if err != nil { + log.Panicf("init exif error: %v", err) + } + defer exifCleanup() + + terminateWorkers := executable_worker.Initialize() + defer terminateWorkers() + + exitCode = m.Run() +} + +func FilesystemTest(t *testing.T) { + if !flags.Filesystem { + t.Skip("Filesystem integration tests disabled") + } + utils.ConfigureTestCache(t.TempDir()) +} + +func DatabaseTest(t *testing.T) *gorm.DB { + if !flags.Database { + t.Skip("Database integration tests disabled") + } + + if err := test_dbm.SetupAndReset(); err != nil { + t.Fatalf("failed to setup or reset test database: %v", err) + } + + return test_dbm.DB +} diff --git a/api/test_utils/scanner/scanner_helpers.go b/api/test_utils/scanner/scanner_helpers.go new file mode 100644 index 0000000..ae9af41 --- /dev/null +++ b/api/test_utils/scanner/scanner_helpers.go @@ -0,0 +1,49 @@ +package scanner_utils + +import ( + "testing" + "time" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/scanner_queue" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func RunScannerOnUser(t *testing.T, db *gorm.DB, user *models.User) { + start := time.Now() + defer func() { + dur := time.Now().Sub(start) + t.Logf("RunScannerOnUser(user(id:%d)) took %s.", user.ID, dur) + }() + + if !assert.NoError(t, scanner_queue.InitializeScannerQueue(db)) { + return + } + + if !assert.NoError(t, scanner_queue.AddUserToQueue(user)) { + return + } + + // wait for all jobs to finish + scanner_queue.CloseScannerQueue() +} + +func RunScannerAll(t *testing.T, db *gorm.DB) { + start := time.Now() + defer func() { + dur := time.Now().Sub(start) + t.Logf("RunScannerAll() took %s.", dur) + }() + + if !assert.NoError(t, scanner_queue.InitializeScannerQueue(db)) { + return + } + + if !assert.NoError(t, scanner_queue.AddAllToQueue()) { + return + } + + // wait for all jobs to finish + scanner_queue.CloseScannerQueue() +} diff --git a/api/test_utils/test_db_manager.go b/api/test_utils/test_db_manager.go new file mode 100644 index 0000000..45a6dc4 --- /dev/null +++ b/api/test_utils/test_db_manager.go @@ -0,0 +1,65 @@ +package test_utils + +import ( + "fmt" + + "github.com/photoview/photoview/api/database" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +type TestDBManager struct { + DB *gorm.DB +} + +func (dbm *TestDBManager) SetupAndReset() error { + if dbm.DB == nil { + if err := dbm.setup(); err != nil { + return fmt.Errorf("setup db error: %w", err) + } + } + + return dbm.reset() +} + +func (dbm *TestDBManager) Close() error { + if dbm.DB == nil { + return nil + } + + sqlDB, err := dbm.DB.DB() + if err != nil { + return fmt.Errorf("get db instance when closing test database error: %w", err) + } + + sqlDB.Close() + dbm.DB = nil + + return nil +} + +func (dbm *TestDBManager) setup() error { + config := gorm.Config{ + Logger: logger.Default.LogMode(logger.Warn), + } + db, err := database.ConfigureDatabase(&config) + if err != nil { + return fmt.Errorf("configure test database error: %w", err) + } + + dbm.DB = db + + return nil +} + +func (dbm *TestDBManager) reset() error { + if err := database.ClearDatabase(dbm.DB); err != nil { + return fmt.Errorf("clean database error: %w", err) + } + + if err := database.MigrateDatabase(dbm.DB); err != nil { + return fmt.Errorf("migrate database error: %w", err) + } + + return nil +} diff --git a/api/utils/endpoints.go b/api/utils/endpoints.go new file mode 100644 index 0000000..6402187 --- /dev/null +++ b/api/utils/endpoints.go @@ -0,0 +1,88 @@ +package utils + +import ( + "log" + "net" + "net/url" + "strconv" + "strings" +) + +const defaultIP = "127.0.0.1" +const defaultPort = "4001" +const defaultAPIPrefix = "/api" + +func ApiListenUrl() *url.URL { + apiPath := ApiEndpointUrl().Path + + var listenAddr string + + listenAddr = EnvListenIP.GetValue() + if listenAddr == "" { + listenAddr = defaultIP + } + // Validate listenAddr is a valid IP (IPv4 or IPv6) + if parsedIP := net.ParseIP(listenAddr); parsedIP == nil { + log.Panicf("%q must be a valid IPv4 or IPv6 address: %q", EnvListenIP.GetName(), listenAddr) + } + + listenPortStr := EnvListenPort.GetValue() + if listenPortStr == "" { + listenPortStr = defaultPort + } + listenPort, err := strconv.Atoi(listenPortStr) + if err != nil { + log.Panicf("%q must be a number %q: %v", EnvListenPort.GetName(), listenPortStr, err) + } + if listenPort < 1 || listenPort > 65535 { + log.Panicf("%q must be a valid port number (1-65535): %d", EnvListenPort.GetName(), listenPort) + } + + return &url.URL{ + Scheme: "http", + Host: net.JoinHostPort(listenAddr, listenPortStr), + Path: apiPath, + } +} + +func ApiEndpointUrl() *url.URL { + apiEndpointStr := EnvAPIEndpoint.GetValue() + if apiEndpointStr == "" { + apiEndpointStr = defaultAPIPrefix + } + + apiEndpointURL, err := url.Parse(apiEndpointStr) + if err != nil { + log.Panicf("ERROR: Environment variable %s is not a proper url (%s): %v", + EnvAPIEndpoint.GetName(), EnvAPIEndpoint.GetValue(), err) + } + + // If absolute URL with empty path (e.g. "https://host"), default to /api for backward compatibility. + if apiEndpointURL.Scheme != "" && apiEndpointURL.Host != "" && apiEndpointURL.Path == "" { + apiEndpointURL.Path = defaultAPIPrefix + } + // Ensure relative paths start with a leading slash. + if apiEndpointURL.Scheme == "" && + apiEndpointURL.Host == "" && + apiEndpointURL.Path != "" && + !strings.HasPrefix(apiEndpointURL.Path, "/") { + apiEndpointURL.Path = "/" + apiEndpointURL.Path + } + + return apiEndpointURL +} + +func UiEndpointUrl() *url.URL { + shouldServeUI := ShouldServeUI() + if shouldServeUI { + return nil + } + + uiEndpointURL, err := url.Parse(EnvUIEndpoint.GetValue()) + if err != nil { + log.Panicf("ERROR: Environment variable %s is not a proper url (%s): %v", + EnvUIEndpoint.GetName(), EnvUIEndpoint.GetValue(), err) + } + + return uiEndpointURL +} diff --git a/api/utils/environment_variables.go b/api/utils/environment_variables.go new file mode 100644 index 0000000..ccda309 --- /dev/null +++ b/api/utils/environment_variables.go @@ -0,0 +1,104 @@ +package utils + +import ( + "os" + "strconv" + "strings" + "time" + + "github.com/photoview/photoview/api/log" +) + +// EnvironmentVariable represents the name of an environment variable used to configure Photoview +type EnvironmentVariable string + +// General options +const ( + EnvDevelopmentMode EnvironmentVariable = "PHOTOVIEW_DEVELOPMENT_MODE" + EnvServeUI EnvironmentVariable = "PHOTOVIEW_SERVE_UI" + EnvUIPath EnvironmentVariable = "PHOTOVIEW_UI_PATH" + EnvMediaCachePath EnvironmentVariable = "PHOTOVIEW_MEDIA_CACHE" + EnvFaceRecognitionModelsPath EnvironmentVariable = "PHOTOVIEW_FACE_RECOGNITION_MODELS_PATH" + EnvMediaProbeTimeout EnvironmentVariable = "PHOTOVIEW_MEDIA_PROBE_TIMEOUT" +) + +// Network related +const ( + EnvListenIP EnvironmentVariable = "PHOTOVIEW_LISTEN_IP" + EnvListenPort EnvironmentVariable = "PHOTOVIEW_LISTEN_PORT" + EnvAPIEndpoint EnvironmentVariable = "PHOTOVIEW_API_ENDPOINT" + EnvUIEndpoint EnvironmentVariable = "PHOTOVIEW_UI_ENDPOINT" +) + +// Database related +const ( + EnvDatabaseDriver EnvironmentVariable = "PHOTOVIEW_DATABASE_DRIVER" + EnvMysqlURL EnvironmentVariable = "PHOTOVIEW_MYSQL_URL" + EnvPostgresURL EnvironmentVariable = "PHOTOVIEW_POSTGRES_URL" + EnvSqlitePath EnvironmentVariable = "PHOTOVIEW_SQLITE_PATH" +) + +// Feature related +const ( + EnvDisableFaceRecognition EnvironmentVariable = "PHOTOVIEW_DISABLE_FACE_RECOGNITION" + EnvDisableVideoEncoding EnvironmentVariable = "PHOTOVIEW_DISABLE_VIDEO_ENCODING" + EnvDisableRawProcessing EnvironmentVariable = "PHOTOVIEW_DISABLE_RAW_PROCESSING" + EnvVideoHardwareAcceleration EnvironmentVariable = "PHOTOVIEW_VIDEO_HARDWARE_ACCELERATION" +) + +// GetName returns the name of the environment variable itself +func (v EnvironmentVariable) GetName() string { + return string(v) +} + +// GetValue returns the value of the environment +func (v EnvironmentVariable) GetValue() string { + return os.Getenv(string(v)) +} + +// GetBool returns the environment variable as a boolean (defaults to false if not defined) +func (v EnvironmentVariable) GetBool() bool { + value := strings.ToLower(os.Getenv(string(v))) + trueValues := []string{"1", "true"} + + for _, x := range trueValues { + if value == x { + return true + } + } + + return false +} + +// ShouldServeUI whether or not the "serve ui" option is enabled +func ShouldServeUI() bool { + return EnvServeUI.GetBool() +} + +// DevelopmentMode describes whether or not the server is running in development mode, +// and should thus print debug informations and enable other features related to developing. +func DevelopmentMode() bool { + return EnvDevelopmentMode.GetBool() +} + +// MediaProbeTimeout returns the media probing timeout duration. +// Defaults to 5 seconds if PHOTOVIEW_MEDIA_PROBE_TIMEOUT is not set or invalid. +// The value is interpreted as seconds. +func MediaProbeTimeout() time.Duration { + if val := EnvMediaProbeTimeout.GetValue(); val != "" { + if seconds, err := strconv.Atoi(val); err == nil && seconds > 0 { + return time.Duration(seconds) * time.Second + } + log.Warn(nil, "Invalid PHOTOVIEW_MEDIA_PROBE_TIMEOUT value, using default 5s", "value", val) + } + return 5 * time.Second +} + +// UIPath returns the value from where the static UI files are located if SERVE_UI=1 +func UIPath() string { + if path := EnvUIPath.GetValue(); path != "" { + return path + } + + return "./ui" +} diff --git a/api/utils/media_cache.go b/api/utils/media_cache.go new file mode 100644 index 0000000..135612c --- /dev/null +++ b/api/utils/media_cache.go @@ -0,0 +1,73 @@ +package utils + +import ( + "os" + "path" + "strconv" + "sync" + + "github.com/pkg/errors" +) + +// CachePathForMedia is a low-level implementation for Media.CachePath() +func CachePathForMedia(albumID int, mediaID int) (string, error) { + + // Make root cache dir if not exists + if _, err := os.Stat(MediaCachePath()); os.IsNotExist(err) { + if err := os.Mkdir(MediaCachePath(), os.ModePerm); err != nil { + return "", errors.Wrap(err, "could not make root image cache directory") + } + } + + // Make album cache dir if not exists + albumCachePath := path.Join(MediaCachePath(), strconv.Itoa(int(albumID))) + if _, err := os.Stat(albumCachePath); os.IsNotExist(err) { + if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil { + return "", errors.Wrap(err, "could not make album image cache directory") + } + } + + // Make photo cache dir if not exists + photoCachePath := path.Join(albumCachePath, strconv.Itoa(int(mediaID))) + if _, err := os.Stat(photoCachePath); os.IsNotExist(err) { + if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil { + return "", errors.Wrap(err, "could not make photo image cache directory") + } + } + + return photoCachePath, nil +} + +var ( + testCachePath string = "" + testCachePathLocker sync.RWMutex +) + +func GetTestCachePath() string { + testCachePathLocker.RLock() + defer testCachePathLocker.RUnlock() + return testCachePath +} + +func ConfigureTestCache(tmpDir string) { + testCachePathLocker.Lock() + defer testCachePathLocker.Unlock() + testCachePath = tmpDir +} + +// MediaCachePath returns the path for where the media cache is located on the file system +func MediaCachePath() string { + testCachePathLocker.RLock() + cachedPath := testCachePath + testCachePathLocker.RUnlock() + if cachedPath != "" { + return cachedPath + } + + photoCache := EnvMediaCachePath.GetValue() + if photoCache == "" { + photoCache = "./media_cache" + } + + return photoCache +} diff --git a/api/utils/throttle.go b/api/utils/throttle.go new file mode 100644 index 0000000..10d2085 --- /dev/null +++ b/api/utils/throttle.go @@ -0,0 +1,25 @@ +package utils + +import "time" + +type Throttle struct { + interval time.Duration + lastAction time.Time +} + +func NewThrottle(interval time.Duration) Throttle { + return Throttle{ + interval: interval, + lastAction: time.Unix(0, 0), + } +} + +func (t *Throttle) Trigger(action func()) { + if action == nil { + return + } + if time.Now().After(t.lastAction.Add(t.interval)) { + t.lastAction = time.Now() + action() + } +} diff --git a/api/utils/utils.go b/api/utils/utils.go new file mode 100644 index 0000000..bb514c1 --- /dev/null +++ b/api/utils/utils.go @@ -0,0 +1,92 @@ +package utils + +import ( + "crypto/rand" + "fmt" + "log" + "math/big" + "os" + "path" + "path/filepath" +) + +func GenerateToken() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const length = 8 + + charLen := big.NewInt(int64(len(charset))) + + b := make([]byte, length) + for i := range b { + + n, err := rand.Int(rand.Reader, charLen) + if err != nil { + log.Panicf("Could not generate random number: %v", err) + } + b[i] = charset[n.Int64()] + } + return string(b) +} + +type PhotoviewError struct { + message string + original error +} + +func (e PhotoviewError) Error() string { + return fmt.Sprintf("%s: %s", e.message, e.original) +} + +func HandleError(message string, err error) PhotoviewError { + log.Printf("ERROR: %s: %s", message, err) + return PhotoviewError{ + message: message, + original: err, + } +} + +var test_face_recognition_models_path string = "" + +func ConfigureTestFaceRecognitionModelsPath(path string) { + test_face_recognition_models_path = path +} + +func FaceRecognitionModelsPath() string { + if test_face_recognition_models_path != "" { + return test_face_recognition_models_path + } + + if EnvFaceRecognitionModelsPath.GetValue() == "" { + return path.Join("data", "models") + } + + return EnvFaceRecognitionModelsPath.GetValue() +} + +// IsDirSymlink checks that the given path is a symlink and resolves to a +// directory. +func IsDirSymlink(linkPath string) (bool, error) { + + fileInfo, err := os.Lstat(linkPath) + if err != nil { + return false, fmt.Errorf("cannot get fileinfo of the symlink %q: %w", linkPath, err) + } + + // Resolve symlinks + if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { + resolvedPath, err := filepath.EvalSymlinks(linkPath) + if err != nil { + return false, fmt.Errorf("cannot resolve symlink target for %q, skipping it: %w", linkPath, err) + } + + resolvedFile, err := os.Stat(resolvedPath) + if err != nil { + return false, fmt.Errorf("cannot get fileinfo of the symlink %q target %q, skipping it: %w", + linkPath, resolvedPath, err) + } + + return resolvedFile.IsDir(), nil + } + + return false, nil +} diff --git a/api/utils/utils_test.go b/api/utils/utils_test.go new file mode 100644 index 0000000..aeb8d69 --- /dev/null +++ b/api/utils/utils_test.go @@ -0,0 +1,76 @@ +package utils_test + +import ( + "os" + "path" + "testing" + + "github.com/photoview/photoview/api/test_utils" + "github.com/photoview/photoview/api/utils" +) + +func TestMain(m *testing.M) { + test_utils.IntegrationTestRun(m) +} + +func TestIsDirSymlink(t *testing.T) { + test_utils.FilesystemTest(t) + + // Prepare a temporary directory for testing purposes + dir, err := os.MkdirTemp("", "testing") + if err != nil { + t.Fatalf("unable to create temp directory for testing") + } + defer os.RemoveAll(dir) + + // Create regular file + _, err = os.Create(path.Join(dir, "regular_file")) + if err != nil { + t.Fatalf("unable to create regular file for testing") + } + + // Create directory + err = os.Mkdir(path.Join(dir, "directory"), 0755) + if err != nil { + t.Fatalf("unable to create directory for testing") + } + + // Create symlink to regular file + err = os.Symlink(path.Join(dir, "regular_file"), path.Join(dir, "file_link")) + if err != nil { + t.Fatalf("unable to create file link for testing") + } + + // Create symlink to directory + err = os.Symlink(path.Join(dir, "directory"), path.Join(dir, "dir_link")) + if err != nil { + t.Fatalf("unable to create dir link for testing") + } + + // Execute the actual tests + + isDirLink, _ := utils.IsDirSymlink(path.Join(dir, "regular_file")) + if isDirLink { + t.Error("Failed detection of regular file") + } + + isDirLink, _ = utils.IsDirSymlink(path.Join(dir, "directory")) + if isDirLink { + t.Error("Failed detection of directory") + } + + isDirLink, _ = utils.IsDirSymlink(path.Join(dir, "file_link")) + if isDirLink { + t.Error("Failed detection of link to regular file") + } + + isDirLink, _ = utils.IsDirSymlink(path.Join(dir, "dir_link")) + if !isDirLink { + t.Error("Failed detection of link to directory") + } + + isDirLink, err = utils.IsDirSymlink(path.Join(dir, "non_existant")) + if err == nil { + t.Error("Missing error for non-existant file") + } +} diff --git a/ui/.eslintignore b/ui/.eslintignore new file mode 100644 index 0000000..0e75fe5 --- /dev/null +++ b/ui/.eslintignore @@ -0,0 +1,3 @@ +node_modules +dist +coverage diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js new file mode 100644 index 0000000..f4d0cca --- /dev/null +++ b/ui/.eslintrc.js @@ -0,0 +1,59 @@ +/* global __dirname */ + +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + env: { + browser: true, + es6: true, + }, + ignorePatterns: ['node_modules', 'dist'], + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'prettier', + ], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + process: 'readonly', + module: 'readonly', + require: 'readonly', + }, + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + plugins: ['react', 'react-hooks', '@typescript-eslint'], + rules: { + 'no-unused-vars': 'off', + 'react/display-name': 'off', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + }, + settings: { + react: { + version: 'detect', + }, + }, + overrides: [ + { + files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-misused-promises': 'off', + }, + }, + ], +} diff --git a/ui/apollo.config.js b/ui/apollo.config.js new file mode 100644 index 0000000..95ef9d3 --- /dev/null +++ b/ui/apollo.config.js @@ -0,0 +1,38 @@ +// apollo.config.js +const { readdirSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs'); +const path = require('node:path'); + +const schemasFolder = path.join(__dirname, '..', 'api', 'graphql', 'resolvers'); + +let completeSchema; +try { + completeSchema = readdirSync(schemasFolder) + .filter(x => x.endsWith('.graphql')) + .map(x => { + const filePath = path.join(schemasFolder, x); + return readFileSync(filePath, 'utf-8'); + }) + .join('\n\n'); +} catch (error) { + console.error('Failed to generate schema:', error); + process.exit(1); +} + +const outputPath = path.join(__dirname, '.cache', 'schema.graphql'); + +try { + mkdirSync(path.dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, completeSchema, { mode: 0o644 }); +} catch (error) { + console.error('Failed to write schema file:', error); + process.exit(1); +} + +module.exports = { + client: { + service: { + name: 'photoview', + localSchemaFile: outputPath, + }, + }, +} diff --git a/ui/example.env b/ui/example.env new file mode 100644 index 0000000..54d0353 --- /dev/null +++ b/ui/example.env @@ -0,0 +1 @@ +REACT_APP_API_ENDPOINT=http://localhost:4001/ \ No newline at end of file diff --git a/ui/i18next-parser.config.js b/ui/i18next-parser.config.js new file mode 100644 index 0000000..4731be3 --- /dev/null +++ b/ui/i18next-parser.config.js @@ -0,0 +1,29 @@ +module.exports = { + defaultValue: function (locale, namespace, key, value) { + if (locale != 'en') { + return ''; + } + return value || key; + }, + sort: true, + locales: [ + 'da', + 'de', + 'en', + 'es', + 'fr', + 'it', + 'ja', + 'nl', + 'pl', + 'pt', + 'ru', + 'sv', + 'uk', + 'zh-CN', + 'zh-HK', + 'zh-TW', + ], + input: 'src/**/*.{js,ts,jsx,tsx}', + output: 'src/extractedTranslations/$LOCALE/$NAMESPACE.json', +} diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..1c0f6f9 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + +
+ + + diff --git a/ui/nodemon.json b/ui/nodemon.json new file mode 100644 index 0000000..a52ac10 --- /dev/null +++ b/ui/nodemon.json @@ -0,0 +1,9 @@ +{ + "watch": ["./**"], + "exec": "npm start", + "ext": "ts,json,tsx,js,html,css", + "ignore": ["./dist/"], + "execMap": { + "ts": "ts-node-esm" + } +} diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..e0690af --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,26430 @@ +{ + "name": "photoview-ui", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "photoview-ui", + "version": "0.0.1", + "license": "GPL-3.0", + "dependencies": { + "@apollo/client": "^3.6.9", + "@babel/preset-typescript": "^7.18.6", + "@headlessui/react": "^1.6.6", + "@types/geojson": "^7946.0.8", + "@types/jest": "^28.1.4", + "@types/js-cookie": "^3.0.6", + "@types/luxon": "^3.7.1", + "@types/mapbox-gl": "^2.7.3", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "@types/react-helmet": "^6.1.5", + "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.25", + "@types/url-join": "^4.0.1", + "@vitejs/plugin-react": "^1.3.2", + "autoprefixer": "^10.4.7", + "blurhash": "^1.1.5", + "classnames": "^2.3.1", + "connect-history-api-fallback": "^2.0.0", + "copy-to-clipboard": "^3.3.1", + "dayjs": "^1.11.19", + "i18next": "^21.8.13", + "js-cookie": "^3.0.5", + "luxon": "^3.7.2", + "mapbox-gl": "^2.9.1", + "postcss": "^8.4.39", + "prettier": "^2.7.1", + "react": "^18.2.0", + "react-blurhash": "^0.1.3", + "react-datepicker": "^9.0.0", + "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", + "react-hook-form": "^7.33.1", + "react-i18next": "^11.18.0", + "react-router-dom": "^6.3.0", + "react-swipeable": "^7.0.0", + "react-test-renderer": "^18.2.0", + "styled-components": "^5.3.5", + "subscriptions-transport-ws": "^0.11.0", + "tailwind-override": "^0.6.1", + "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17", + "url-join": "^5.0.0", + "vite": "^2.9.13", + "vite-plugin-svgr": "^2.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^14.2.1", + "@typescript-eslint/eslint-plugin": "^5.30.6", + "@typescript-eslint/parser": "^5.30.6", + "@vitest/ui": "^0.17.1", + "apollo": "2.34.0", + "apollo-language-server": "1.26.9", + "c8": "^7.11.3", + "eslint": "^8.19.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-react": "^7.37.1", + "eslint-plugin-react-hooks": "^4.6.2", + "i18next-parser": "^8.13.0", + "jsdom": "^20.0.0", + "lint-staged": "^13.0.3", + "nodemon": "^3.1.4", + "ts-node": "^10.9.2", + "typescript": "^5.5.3", + "vitest": "^0.17.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apollo/client": { + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz", + "integrity": "sha512-Y1yu8qa2YeaCUBVuw08x8NHenFi0sw2I3KCu7Kw9mDSu86HmmtHJkCAifKVrN2iPgDTW/BbP3EpSV8/EQCcxZA==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/context": "^0.6.0", + "@wry/equality": "^0.5.0", + "@wry/trie": "^0.3.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.16.1", + "prop-types": "^15.7.2", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql-ws": "^5.5.5", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, + "node_modules/@apollo/federation": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.27.0.tgz", + "integrity": "sha512-hMeRN9IPsIn+5J5SmWof0ODbvRjRj8mBNqbsm9Zjkqjbw6RTlcx90taMk7cYhcd/E+uTyLQt5cOSRVBx53cxbQ==", + "dev": true, + "dependencies": { + "apollo-graphql": "^0.9.3", + "lodash.xorby": "^4.7.0" + }, + "engines": { + "node": ">=12.13.0 <17.0" + }, + "peerDependencies": { + "graphql": "^14.5.0 || ^15.0.0" + } + }, + "node_modules/@apollo/utils.keyvaluecache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.1.tgz", + "integrity": "sha512-nLgYLomqjVimEzQ4cdvVQkcryi970NDvcRVPfd0OPeXhBfda38WjBq+WhQFk+czSHrmrSp34YHBxpat0EtiowA==", + "dev": true, + "dependencies": { + "@apollo/utils.logger": "^1.0.0", + "lru-cache": "^7.10.1" + } + }, + "node_modules/@apollo/utils.keyvaluecache/node_modules/lru-cache": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.12.0.tgz", + "integrity": "sha512-OIP3DwzRZDfLg9B9VP/huWBlpvbkmbfiBy8xmsXp4RPmE4A3MhwNozc5ZJ3fWnSg8fDcdlE/neRTPG2ycEKliw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@apollo/utils.logger": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-1.0.0.tgz", + "integrity": "sha512-dx9XrjyisD2pOa+KsB5RcDbWIAdgC91gJfeyLCgy0ctJMjQe7yZK5kdWaWlaOoCeX0z6YI9iYlg7vMPyMpQF3Q==", + "dev": true + }, + "node_modules/@apollographql/apollo-tools": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.4.tgz", + "integrity": "sha512-shM3q7rUbNyXVVRkQJQseXv6bnYM3BUma/eZhwXR4xsuM+bqWnJKvW7SAfRjP7LuSCocrexa5AXhjjawNHrIlw==", + "dev": true, + "engines": { + "node": ">=8", + "npm": ">=6" + }, + "peerDependencies": { + "graphql": "^14.2.1 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@apollographql/graphql-language-service-interface": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-interface/-/graphql-language-service-interface-2.0.2.tgz", + "integrity": "sha512-28wePK0hlIVjgmvMXMAUq8qRSjz9O+6lqFp4PzOTHtfJfSsjVe9EfjF98zTpHsTgT3HcOxmbqDZZy8jlXtOqEA==", + "dev": true, + "dependencies": { + "@apollographql/graphql-language-service-parser": "^2.0.0", + "@apollographql/graphql-language-service-types": "^2.0.0", + "@apollographql/graphql-language-service-utils": "^2.0.2" + }, + "peerDependencies": { + "graphql": "^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + } + }, + "node_modules/@apollographql/graphql-language-service-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-parser/-/graphql-language-service-parser-2.0.2.tgz", + "integrity": "sha512-rpTPrEJu1PMaRQxz5P8BZWsixNNhYloS0H0dwTxNBuE3qctbARvR7o8UCKLsmKgTbo+cz3T3a6IAsWlkHgMWGg==", + "dev": true, + "dependencies": { + "@apollographql/graphql-language-service-types": "^2.0.0" + }, + "peerDependencies": { + "graphql": "^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + } + }, + "node_modules/@apollographql/graphql-language-service-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-types/-/graphql-language-service-types-2.0.2.tgz", + "integrity": "sha512-vE+Dz8pG+Xa1Z2nMl82LoO66lQ6JqBUjaXqLDvS3eMjvA3N4hf+YUDOWfPdNZ0zjhHhHXzUIIZCkax6bXfFbzQ==", + "dev": true, + "peerDependencies": { + "graphql": "^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + } + }, + "node_modules/@apollographql/graphql-language-service-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-utils/-/graphql-language-service-utils-2.0.2.tgz", + "integrity": "sha512-fDj5rWlTi/czvUS5t7V7I45Ai6bOO3Z7JARYj21Y2xxfbRGtJi6h8FvLX0N/EbzQgo/fiZc/HAhtfwn+OCjD7A==", + "dev": true, + "dependencies": { + "@apollographql/graphql-language-service-types": "^2.0.0" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.6.tgz", + "integrity": "sha512-tzulrgDT0QD6U7BJ4TKVk2SDDg7wlP39P9yAx1RfLy7vP/7rsDRlWVfbWxElslu56+r7QOhB2NSDsabYYruoZQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.6.tgz", + "integrity": "sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.6", + "@babel/helper-compilation-targets": "^7.18.6", + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helpers": "^7.18.6", + "@babel/parser": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz", + "integrity": "sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg==", + "dependencies": { + "@babel/compat-data": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.6.tgz", + "integrity": "sha512-YfDzdnoxHGV8CzqHGyCbFvXg5QESPFkXlHtvdCkesLjjVMT2Adxe4FGUR5ChIb3DxSaXO12iIOCWoXdsUVwnqw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.6", + "@babel/helper-function-name": "^7.18.6", + "@babel/helper-member-expression-to-functions": "^7.18.6", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.6.tgz", + "integrity": "sha512-CeHxqwwipekotzPDUuJOfIMtcIHBuc7WAzLmTYWctVigqS5RktNMQ5bEwQSuGewzYnCtTWa3BARXeiLxDTv+Ng==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.6.tgz", + "integrity": "sha512-L//phhB4al5uucwzlimruukHB3jRd5JGClwRMD/ROrVjXfLqovYnvQrK/JK36WYyVwGGO7OD3kMyVTjx+WVPhw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz", + "integrity": "sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.18.6.tgz", + "integrity": "sha512-fTf7zoXnUGl9gF25fXCWE26t7Tvtyn6H4hkLSYhATwJvw2uYxd3aoXplMSe0g9XbwK7bmxNes7+FGO0rB/xC0g==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.6", + "@babel/helper-member-expression-to-functions": "^7.18.6", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.6.tgz", + "integrity": "sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==", + "dependencies": { + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", + "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz", + "integrity": "sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", + "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz", + "integrity": "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz", + "integrity": "sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.6.tgz", + "integrity": "sha512-ijHNhzIrLj5lQCnI6aaNVRtGVuUZhOXFLRVFs7lLrkXTHip4FKty5oAuQdk4tywG0/WjXmjTfQCWmuzrvFer1w==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-typescript": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", + "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-typescript": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.3.tgz", + "integrity": "sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA==", + "dependencies": { + "@emotion/memoize": "^0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz", + "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "node_modules/@endemolshinegroup/cosmiconfig-typescript-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", + "integrity": "sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==", + "dev": true, + "dependencies": { + "lodash.get": "^4", + "make-error": "^1", + "ts-node": "^9", + "tslib": "^2" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "cosmiconfig": ">=6" + } + }, + "node_modules/@endemolshinegroup/cosmiconfig-typescript-loader/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/@endemolshinegroup/cosmiconfig-typescript-loader/node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "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/globals": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", + "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@headlessui/react": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.6.tgz", + "integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "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/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.0.2.tgz", + "integrity": "sha512-YVDJZjd4izeTDkij00vHHAymNXQ6WWsdChFRK86qck6Jpr3DCL5W3Is3vslviRlP+bLuMYRLbdp98amMvqudhA==", + "dependencies": { + "@sinclair/typebox": "^0.23.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.1.tgz", + "integrity": "sha512-eL7fMmfTBKjrb+VFHXCGv9Ot0zc3C0U+CwXo1IrP+EPwDczLoXv34Tgq3y+2mPSFNVUXgU42ILWJTC7145KPTA==", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/geojson-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", + "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==" + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha1-zlblOfg1UrWNENZy6k1vya3HsjQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI=" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz", + "integrity": "sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw==" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha1-FWUb1VOme4WB+zmIEMmK2Go0Uk4=" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oclif/color": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@oclif/color/-/color-1.0.1.tgz", + "integrity": "sha512-qjYr+izgWdIVOroiBKqTzQgc1r5Wd9QB1J7yGM2EeelqhBARiiVLRZL45vhV4zdyTRdDkZS0EBzFwQap+nliLA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.2.1", + "chalk": "^4.1.0", + "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "tslib": "^2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/color/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@oclif/color/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@oclif/color/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@oclif/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@oclif/color/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/color/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@oclif/command": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.16.tgz", + "integrity": "sha512-rmVKYEsKzurfRU0xJz+iHelbi1LGlihIWZ7Qvmb/CBz1EkhL7nOkW4SVXmG2dA5Ce0si2gr88i6q4eBOMRNJ1w==", + "dev": true, + "dependencies": { + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "@oclif/help": "^1.0.1", + "@oclif/parser": "^3.8.6", + "debug": "^4.1.1", + "semver": "^7.3.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@oclif/config": "^1" + } + }, + "node_modules/@oclif/command/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/config": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.18.3.tgz", + "integrity": "sha512-sBpko86IrTscc39EvHUhL+c++81BVTsIZ3ETu/vG+cCdi0N6vb2DoahR67A9FI2CGnxRRHjnTfa3m6LulwNATA==", + "dev": true, + "dependencies": { + "@oclif/errors": "^1.3.5", + "@oclif/parser": "^3.8.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-wsl": "^2.1.1", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@oclif/core": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.9.5.tgz", + "integrity": "sha512-C605Cr4RhHTMXYApLxdVt/PL6IA5cguN6MPvsMjxkvBppk2Fvcsj05dtRdDqShskRpZzHtu65emG1tHy8TWPWQ==", + "dev": true, + "dependencies": { + "@oclif/linewrap": "^1.0.0", + "@oclif/screen": "^3.0.2", + "ansi-escapes": "^4.3.2", + "ansi-styles": "^4.3.0", + "cardinal": "^2.1.1", + "chalk": "^4.1.2", + "clean-stack": "^3.0.1", + "cli-progress": "^3.10.0", + "debug": "^4.3.4", + "ejs": "^3.1.6", + "fs-extra": "^9.1.0", + "get-package-type": "^0.1.0", + "globby": "^11.1.0", + "hyperlinker": "^1.0.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "js-yaml": "^3.14.1", + "natural-orderby": "^2.0.3", + "object-treeify": "^1.1.33", + "password-prompt": "^1.1.2", + "semver": "^7.3.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "supports-hyperlinks": "^2.2.0", + "tslib": "^2.3.1", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@oclif/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@oclif/core/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@oclif/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@oclif/core/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/core/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/core/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@oclif/errors": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.5.tgz", + "integrity": "sha512-OivucXPH/eLLlOT7FkCMoZXiaVYf8I/w1eTAM1+gKzfhALwWTusxEx7wBmW0uzvkSg/9ovWLycPaBgJbM3LOCQ==", + "dev": true, + "dependencies": { + "clean-stack": "^3.0.0", + "fs-extra": "^8.1", + "indent-string": "^4.0.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@oclif/errors/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@oclif/errors/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@oclif/errors/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@oclif/help": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@oclif/help/-/help-1.0.1.tgz", + "integrity": "sha512-8rsl4RHL5+vBUAKBL6PFI3mj58hjPCp2VYyXD4TAa7IMStikFfOH2gtWmqLzIlxAED2EpD0dfYwo9JJxYsH7Aw==", + "dev": true, + "dependencies": { + "@oclif/config": "1.18.2", + "@oclif/errors": "1.3.5", + "chalk": "^4.1.2", + "indent-string": "^4.0.0", + "lodash": "^4.17.21", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@oclif/help/node_modules/@oclif/config": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.18.2.tgz", + "integrity": "sha512-cE3qfHWv8hGRCP31j7fIS7BfCflm/BNZ2HNqHexH+fDrdF2f1D5S8VmXWLC77ffv3oDvWyvE9AZeR0RfmHCCaA==", + "dev": true, + "dependencies": { + "@oclif/errors": "^1.3.3", + "@oclif/parser": "^3.8.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-wsl": "^2.1.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@oclif/help/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@oclif/help/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@oclif/help/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@oclif/help/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@oclif/help/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/help/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/help/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/linewrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oclif/linewrap/-/linewrap-1.0.0.tgz", + "integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw==", + "dev": true + }, + "node_modules/@oclif/parser": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.7.tgz", + "integrity": "sha512-b11xBmIUK+LuuwVGJpFs4LwQN2xj2cBWj2c4z1FtiXGrJ85h9xV6q+k136Hw0tGg1jQoRXuvuBnqQ7es7vO9/Q==", + "dev": true, + "dependencies": { + "@oclif/errors": "^1.3.5", + "@oclif/linewrap": "^1.0.0", + "chalk": "^4.1.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@oclif/parser/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@oclif/parser/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@oclif/parser/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@oclif/parser/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@oclif/parser/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/parser/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-1.3.0.tgz", + "integrity": "sha512-N2DRWKvuSXTGuaYf4buRbRfh5yNybb1cjQmPl9viY0BIqTwZgtQdzSD6ZSOkwda51RbGcQomYcc/h8T+ZFAkMQ==", + "dev": true, + "dependencies": { + "@oclif/core": "^1.7.0", + "chalk": "^4.1.0", + "debug": "^4.3.4", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-5.1.12.tgz", + "integrity": "sha512-HvH/RubJxqCinP0vUWQLTOboT+SfjfL8h40s+PymkWaldIcXlpoRaJX50vz+SjZIs7uewZwEk8fzLqpF/BWXlg==", + "dev": true, + "dependencies": { + "@oclif/core": "^1.3.6" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-2.3.1.tgz", + "integrity": "sha512-AeNBw+zSkRpePmpXO8xlL072VF2/R2yK3qsVs/JF26Yw1w77TWuRTdFR+hFotJtFCJ4QYqhNtKSjdryCO9AXsA==", + "dev": true, + "dependencies": { + "@oclif/color": "^1.0.0", + "@oclif/core": "^1.2.1", + "fast-levenshtein": "^3.0.0", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-not-found/node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/@oclif/plugin-plugins": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-2.1.0.tgz", + "integrity": "sha512-Bgt+QpTlX7+Q0HkVgtbUGYQlo/hyzNBAaXH5l16ou9Ji5wfi5T+niV5AzQ14R7JF8ZDOTbUOU/NRBJ2bzLCaZQ==", + "dev": true, + "dependencies": { + "@oclif/color": "^1.0.1", + "@oclif/core": "^1.2.0", + "chalk": "^4.1.2", + "debug": "^4.1.0", + "fs-extra": "^9.0", + "http-call": "^5.2.2", + "load-json-file": "^5.3.0", + "npm-run-path": "^4.0.1", + "semver": "^7.3.2", + "tslib": "^2.0.0", + "yarn": "^1.22.17" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-plugins/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@oclif/plugin-plugins/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@oclif/plugin-plugins/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@oclif/plugin-plugins/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@oclif/plugin-plugins/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/plugin-plugins/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/plugin-plugins/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/plugin-plugins/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.0.4.tgz", + "integrity": "sha512-9dprC1CWPjesg0Vf/rDSQH2tzJXhP1ow84cb2My1kj6e6ESulPKpctiCFSZ1WaCQFfq+crKhzlNoP/vRaXNUAg==", + "dev": true, + "dependencies": { + "@oclif/core": "^1.0.8", + "chalk": "^4.1.0", + "debug": "^4.1.0", + "fs-extra": "^9.0.1", + "http-call": "^5.2.2", + "lodash": "^4.17.21", + "semver": "^7.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/screen": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-3.0.2.tgz", + "integrity": "sha512-S/SF/XYJeevwIgHFmVDAFRUvM3m+OjhvCAYMk78ZJQCYCQ5wS7j+LTt1ZEv2jpEEGg2tx/F6TYYWxddNAYHrFQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@samverschueren/stream-to-observable": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", + "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", + "dev": true, + "dependencies": { + "any-observable": "^0.3.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + }, + "zen-observable": { + "optional": true + } + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.23.5.tgz", + "integrity": "sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg==" + }, + "node_modules/@testing-library/dom": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz", + "integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz", + "integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.3.0.tgz", + "integrity": "sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.2.1.tgz", + "integrity": "sha512-HOr1QiODrq+0j9lKU5i10y9TbhxMBMRMGimNx10asdmau9cb8Xb1Vyg0GvTwyIL2ziQyh2kAloOtAQFBQVuecA==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "devOptional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "devOptional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "devOptional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "devOptional": true + }, + "node_modules/@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", + "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/jest": { + "version": "28.1.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.4.tgz", + "integrity": "sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==", + "dependencies": { + "jest-matcher-utils": "^28.0.0", + "pretty-format": "^28.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", + "integrity": "sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==", + "dependencies": { + "@jest/schemas": "^28.0.2", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, + "node_modules/@types/mapbox-gl": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.3.tgz", + "integrity": "sha512-XdveeJptNNZw7ZoeiAJ2/dupNtWaV6qpBG/SOFEpQNQAc+oiO6qUznX85n+W1XbLeD8SVRVfVORKuR+I4CHDZw==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" + }, + "node_modules/@types/react": { + "version": "18.0.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", + "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", + "integrity": "sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-helmet": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.5.tgz", + "integrity": "sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.18", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz", + "integrity": "sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/@types/styled-components": { + "version": "5.1.25", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.25.tgz", + "integrity": "sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/symlink-or-copy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz", + "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", + "dev": true + }, + "node_modules/@types/testing-library__jest-dom": { + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz", + "integrity": "sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg==", + "dev": true, + "dependencies": { + "@types/jest": "*" + } + }, + "node_modules/@types/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-wDXw9LEEUHyV+7UWy7U315nrJGJ7p1BzaCxDpEoLr789Dk1WDVMMlf3iBfbG2F8NdWnYyFbtTxUn2ZNbm1Q4LQ==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", + "integrity": "sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/type-utils": "5.30.6", + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz", + "integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.6.tgz", + "integrity": "sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz", + "integrity": "sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz", + "integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dependencies": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vitest/ui": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.17.1.tgz", + "integrity": "sha512-B4PGDk5IZ10HT9GzR0NQ96VEphWY9dTf1yqBGNjPk2c7wQnhZJdHzv3tgFQiyFK5YR1cjtOV918QHCnptV4r5w==", + "dev": true, + "dependencies": { + "sirv": "^2.0.2" + } + }, + "node_modules/@wry/context": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", + "integrity": "sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.2.tgz", + "integrity": "sha512-oVMxbUXL48EV/C0/M7gLVsoK6qRHPS85x8zECofEZOVvxGmIPLA9o5Z27cc2PoAyZz1S2VoM2A7FLAnpfGlneA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.1.tgz", + "integrity": "sha512-WwB53ikYudh9pIorgxrkHKrQZcCqNM/Q/bDzZBffEaGUKGuHrRb3zZUT9Sh2qw9yogC7SsdRmQ1ER0pqvd3bfw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-globals/node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-node/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, + "node_modules/any-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", + "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apollo": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/apollo/-/apollo-2.34.0.tgz", + "integrity": "sha512-gDH+WBN+b6TA/tIrIuAyO6Df4tsHwAA/t3NZqUitOM0gKo/nXNOUZzskAFTjErL6fgp5+kYIP3rZ+bIleqXAKg==", + "dev": true, + "dependencies": { + "@apollographql/apollo-tools": "0.5.4", + "@oclif/command": "1.8.16", + "@oclif/config": "1.18.3", + "@oclif/errors": "1.3.5", + "@oclif/plugin-autocomplete": "1.3.0", + "@oclif/plugin-help": "5.1.12", + "@oclif/plugin-not-found": "2.3.1", + "@oclif/plugin-plugins": "2.1.0", + "@oclif/plugin-warn-if-update-available": "2.0.4", + "apollo-codegen-core": "0.40.9", + "apollo-codegen-flow": "0.38.9", + "apollo-codegen-scala": "0.39.9", + "apollo-codegen-swift": "0.40.9", + "apollo-codegen-typescript": "0.40.9", + "apollo-env": "0.10.2", + "apollo-graphql": "0.9.7", + "apollo-language-server": "1.26.9", + "chalk": "4.1.2", + "cli-ux": "6.0.9", + "env-ci": "7.1.0", + "gaze": "1.1.3", + "git-parse": "2.1.1", + "git-rev-sync": "3.0.2", + "git-url-parse": "11.6.0", + "glob": "8.0.1", + "global-agent": "3.0.0", + "graphql": "14.0.2 - 14.2.0 || ^14.3.1 || ^15.0.0", + "graphql-tag": "2.12.6", + "listr": "0.14.3", + "lodash.identity": "3.0.0", + "lodash.pickby": "4.6.0", + "mkdirp": "1.0.4", + "moment": "2.29.3", + "strip-ansi": "6.0.1", + "table": "6.8.0", + "tty": "1.0.1", + "vscode-uri": "1.0.6" + }, + "bin": { + "apollo": "bin/run" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/apollo-codegen-core": { + "version": "0.40.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-core/-/apollo-codegen-core-0.40.9.tgz", + "integrity": "sha512-AiynL9PWGZ9zXq9gbJENGixrbmJTORjg8T15gXlPbFcXJzVlQ8+gGuBcHMjBBFBtqb1ZhXN2IZ6udzrRHCB+ag==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.10", + "@babel/parser": "^7.1.3", + "@babel/types": "7.17.10", + "apollo-env": "^0.10.2", + "apollo-language-server": "^1.26.9", + "ast-types": "^0.14.0", + "common-tags": "^1.5.1", + "recast": "^0.21.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/apollo-codegen-core/node_modules/@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/apollo-codegen-core/node_modules/@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/apollo-codegen-core/node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/apollo-codegen-flow": { + "version": "0.38.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-flow/-/apollo-codegen-flow-0.38.9.tgz", + "integrity": "sha512-w02FRiDCfFH7FxRqKZlnmH6q4URT3hlrGvizaRKirEyFQWH7OSEE4osYhSCU+dcRru+NuWtPVrMh3LTB7NinSQ==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.10", + "@babel/types": "7.17.10", + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/apollo-codegen-flow/node_modules/@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/apollo-codegen-flow/node_modules/@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/apollo-codegen-flow/node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/apollo-codegen-scala": { + "version": "0.39.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-scala/-/apollo-codegen-scala-0.39.9.tgz", + "integrity": "sha512-Dtpg8m3MgJ5RIlkPfGDOclsZro1scR32AQY517uA3QdUHa/R+XxU9CQ2bnPnI7BtzuUsrTiJnBXQSulfxrdDOQ==", + "dev": true, + "dependencies": { + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/apollo-codegen-swift": { + "version": "0.40.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-swift/-/apollo-codegen-swift-0.40.9.tgz", + "integrity": "sha512-Ghk0ef4//QOUdJ80kheD7Q20o9UDrXQVWXz8lWUM88w1cba5LBLXz+CeeQ+VyUHrnFO9XqkimqyPZSDpDmHUSA==", + "dev": true, + "dependencies": { + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/apollo-codegen-typescript": { + "version": "0.40.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-typescript/-/apollo-codegen-typescript-0.40.9.tgz", + "integrity": "sha512-koOS3ZbU8UNoZwl87WBxpo+3t0e/iIIkbgYg9zOVKnHCYHi2/CbSE7rq3uAM99QmvcE62wrIoFjpBQADJq78Dw==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.10", + "@babel/types": "7.17.10", + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/apollo-codegen-typescript/node_modules/@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/apollo-codegen-typescript/node_modules/@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/apollo-codegen-typescript/node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/apollo-datasource": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.3.2.tgz", + "integrity": "sha512-L5TiS8E2Hn/Yz7SSnWIVbZw0ZfEIXZCa5VUiVxD9P53JvSrf4aStvsFDlGWPvpIdCR+aly2CfoB79B9/JjKFqg==", + "dev": true, + "dependencies": { + "@apollo/utils.keyvaluecache": "^1.0.1", + "apollo-server-env": "^4.2.1" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/apollo-env": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.10.2.tgz", + "integrity": "sha512-DlB3ZE0j4eFWAVi14pONNPR6h54FlJwerQeuYAxXm0kPA4sGaScsGDRLsIVMP81JxjjDDmWukw5jp9H+zy39hA==", + "dev": true, + "dependencies": { + "@types/node-fetch": "^2.5.10", + "core-js": "^3.0.1", + "node-fetch": "^2.6.1", + "sha.js": "^2.4.11" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/apollo-graphql": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.9.7.tgz", + "integrity": "sha512-bezL9ItUWUGHTm1bI/XzIgiiZbhXpsC7uxk4UxFPmcVJwJsDc3ayZ99oXxAaK+3Rbg/IoqrHckA6CwmkCsbaSA==", + "dev": true, + "dependencies": { + "core-js-pure": "^3.10.2", + "lodash.sortby": "^4.7.0", + "sha.js": "^2.4.11" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "graphql": "^14.2.1 || ^15.0.0" + } + }, + "node_modules/apollo-language-server": { + "version": "1.26.9", + "resolved": "https://registry.npmjs.org/apollo-language-server/-/apollo-language-server-1.26.9.tgz", + "integrity": "sha512-+moe6KfDPPHUaC5Te4x9O5OqBPTZmkNRfjM4kb3XRb3ve8tUeKdye5lIANU+XCv7aZ6G68PHozrO5/Tj1X8Qcw==", + "dev": true, + "dependencies": { + "@apollo/federation": "0.27.0", + "@apollographql/apollo-tools": "^0.5.4", + "@apollographql/graphql-language-service-interface": "^2.0.2", + "@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2", + "apollo-datasource": "^3.0.0", + "apollo-env": "^0.10.2", + "apollo-graphql": "^0.9.7", + "apollo-link": "^1.2.3", + "apollo-link-context": "^1.0.9", + "apollo-link-error": "^1.1.1", + "apollo-link-http": "^1.5.5", + "apollo-server-errors": "^2.0.2", + "await-to-js": "^3.0.0", + "core-js": "^3.0.1", + "cosmiconfig": "^7.0.1", + "dotenv": "^16.0.0", + "glob": "^8.0.0", + "graphql": "14.0.2 - 14.2.0 || ^14.3.1 || ^15.0.0", + "graphql-tag": "^2.10.1", + "lodash.debounce": "^4.0.8", + "lodash.merge": "^4.6.1", + "minimatch": "^5.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.4", + "vscode-uri": "1.0.6" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/apollo-language-server/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/apollo-language-server/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/apollo-language-server/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/apollo-link": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", + "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", + "dev": true, + "dependencies": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.21" + }, + "peerDependencies": { + "graphql": "^0.11.3 || ^0.12.3 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/apollo-link-context": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/apollo-link-context/-/apollo-link-context-1.0.20.tgz", + "integrity": "sha512-MLLPYvhzNb8AglNsk2NcL9AvhO/Vc9hn2ZZuegbhRHGet3oGr0YH9s30NS9+ieoM0sGT11p7oZ6oAILM/kiRBA==", + "dev": true, + "dependencies": { + "apollo-link": "^1.2.14", + "tslib": "^1.9.3" + } + }, + "node_modules/apollo-link-context/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/apollo-link-error": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/apollo-link-error/-/apollo-link-error-1.1.13.tgz", + "integrity": "sha512-jAZOOahJU6bwSqb2ZyskEK1XdgUY9nkmeclCrW7Gddh1uasHVqmoYc4CKdb0/H0Y1J9lvaXKle2Wsw/Zx1AyUg==", + "dev": true, + "dependencies": { + "apollo-link": "^1.2.14", + "apollo-link-http-common": "^0.2.16", + "tslib": "^1.9.3" + } + }, + "node_modules/apollo-link-error/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/apollo-link-http": { + "version": "1.5.17", + "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz", + "integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==", + "dev": true, + "dependencies": { + "apollo-link": "^1.2.14", + "apollo-link-http-common": "^0.2.16", + "tslib": "^1.9.3" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/apollo-link-http-common": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", + "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", + "dev": true, + "dependencies": { + "apollo-link": "^1.2.14", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/apollo-link-http-common/node_modules/ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "dev": true, + "dependencies": { + "tslib": "^1.9.3" + } + }, + "node_modules/apollo-link-http-common/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/apollo-link-http/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/apollo-link/node_modules/ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "dev": true, + "dependencies": { + "tslib": "^1.9.3" + } + }, + "node_modules/apollo-link/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/apollo-link/node_modules/zen-observable-ts": { + "version": "0.8.21", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", + "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", + "dev": true, + "dependencies": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } + }, + "node_modules/apollo-server-env": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-4.2.1.tgz", + "integrity": "sha512-vm/7c7ld+zFMxibzqZ7SSa5tBENc4B0uye9LTfjJwGoQFY5xsUPH5FpO5j0bMUDZ8YYNbrF9SNtzc5Cngcr90g==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/apollo-server-errors": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.5.0.tgz", + "integrity": "sha512-lO5oTjgiC3vlVg2RKr3RiXIIQ5pGXBFxYGGUkKDhTud3jMIhs+gel8L8zsEjKaKxkjHhCQAA/bcEfYiKkGQIvA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/apollo-utilities": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", + "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", + "dev": true, + "dependencies": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/apollo-utilities/node_modules/@wry/equality": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", + "integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==", + "dev": true, + "dependencies": { + "tslib": "^1.9.3" + } + }, + "node_modules/apollo-utilities/node_modules/ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "dev": true, + "dependencies": { + "tslib": "^1.9.3" + } + }, + "node_modules/apollo-utilities/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/apollo/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/apollo/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/apollo/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/apollo/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/apollo/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/apollo/node_modules/glob": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz", + "integrity": "sha512-cF7FYZZ47YzmCu7dDy50xSRRfO3ErRfrXuLZcNIuyiJEco0XSrGtuilG19L5xp3NcwTx7Gn+X6Tv3fmsUPTbow==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/apollo/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/apollo/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/apollo/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz", + "integrity": "sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", + "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.20.3", + "caniuse-lite": "^1.0.30001335", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz", + "integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blurhash": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-1.1.5.tgz", + "integrity": "sha512-a+LO3A2DfxTaTztsmkbLYmUzUeApi0LZuKalwbNmqAHR6HhJGMt1qSV/R3wc+w4DL28holjqO3Bg74aUGavGjg==" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/broccoli-node-api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", + "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", + "dev": true + }, + "node_modules/broccoli-node-info": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", + "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", + "dev": true, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/broccoli-output-wrapper": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", + "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", + "dev": true, + "dependencies": { + "fs-extra": "^8.1.0", + "heimdalljs-logger": "^0.1.10", + "symlink-or-copy": "^1.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/broccoli-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", + "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-output-wrapper": "^3.2.5", + "fs-merger": "^3.2.1", + "promise-map-series": "^0.3.0", + "quick-temp": "^0.1.8", + "rimraf": "^3.0.2", + "symlink-or-copy": "^1.3.1" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c8": { + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz", + "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/c8/node_modules/v8-to-istanbul": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", + "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "dev": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dev": true, + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, + "node_modules/clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clean-stack/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-progress": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.11.2.tgz", + "integrity": "sha512-lCPoS6ncgX4+rJu5bS3F/iCz17kZ9MPZ6dpuTtI0KXKABkhyXIdYB3Inby1OpaGti3YlI3EeEkM9AuWpelJrVA==", + "dev": true, + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.0.tgz", + "integrity": "sha512-7x54QnN21P+XL/v8SuNKvfgsUre6PXpN7mc77N3HlZv+f1SBRGmjxtOud2Z6FZ8DmdkD/IdjCaf9XXbnqmTZGQ==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-ux": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.9.tgz", + "integrity": "sha512-0Ku29QLf+P6SeBNWM7zyoJ49eKKOjxZBZ4OH2aFeRtC0sNXU3ftdJxQPKJ1SJ+axX34I1NsfTFahpXdnxklZgA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "dependencies": { + "@oclif/core": "^1.1.1", + "@oclif/linewrap": "^1.0.0", + "@oclif/screen": "^1.0.4 ", + "ansi-escapes": "^4.3.0", + "ansi-styles": "^4.2.0", + "cardinal": "^2.1.1", + "chalk": "^4.1.0", + "clean-stack": "^3.0.0", + "cli-progress": "^3.10.0", + "extract-stack": "^2.0.0", + "fs-extra": "^8.1", + "hyperlinker": "^1.0.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "js-yaml": "^3.13.1", + "lodash": "^4.17.21", + "natural-orderby": "^2.0.1", + "object-treeify": "^1.1.4", + "password-prompt": "^1.1.2", + "semver": "^7.3.2", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "supports-color": "^8.1.0", + "supports-hyperlinks": "^2.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cli-ux/node_modules/@oclif/screen": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-1.0.4.tgz", + "integrity": "sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/cli-ux/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-ux/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-ux/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-ux/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cli-ux/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cli-ux/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/cli-ux/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-ux/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/cli-ux/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-ux/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cli-ux/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/color-string": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", + "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/core-js": { + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.3.tgz", + "integrity": "sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.1.tgz", + "integrity": "sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "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/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "engines": { + "node": "*" + } + }, + "node_modules/css-select": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", + "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^5.1.0", + "domhandler": "^4.3.0", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-to-react-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", + "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" + }, + "node_modules/css-what": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true + }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha1-s085HupNqPPpgjHizNjfnAQfFxs=" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", + "dev": true + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz", + "integrity": "sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw==", + "dev": true + }, + "node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", + "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/earcut": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz", + "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug==" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.733", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz", + "integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ==" + }, + "node_modules/elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-ci": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-7.1.0.tgz", + "integrity": "sha512-zyRGZQkjp5lgYYRUJS7hbEAhEtfslzwN5ScSnLXhaF2OEtiVC8LW+5mbaIqlFpIE95iFhukrKaLm0Rdt/w2lNg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "fromentries": "^1.3.2", + "java-properties": "^1.0.0" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.48.tgz", + "integrity": "sha512-w6N1Yn5MtqK2U1/WZTX9ZqUVb8IOLZkZ5AdHkT6x3cHDMVsYWC7WPdiLmx19w3i4Rwzy5LqsEMtVihG3e4rFzA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.48", + "esbuild-android-arm64": "0.14.48", + "esbuild-darwin-64": "0.14.48", + "esbuild-darwin-arm64": "0.14.48", + "esbuild-freebsd-64": "0.14.48", + "esbuild-freebsd-arm64": "0.14.48", + "esbuild-linux-32": "0.14.48", + "esbuild-linux-64": "0.14.48", + "esbuild-linux-arm": "0.14.48", + "esbuild-linux-arm64": "0.14.48", + "esbuild-linux-mips64le": "0.14.48", + "esbuild-linux-ppc64le": "0.14.48", + "esbuild-linux-riscv64": "0.14.48", + "esbuild-linux-s390x": "0.14.48", + "esbuild-netbsd-64": "0.14.48", + "esbuild-openbsd-64": "0.14.48", + "esbuild-sunos-64": "0.14.48", + "esbuild-windows-32": "0.14.48", + "esbuild-windows-64": "0.14.48", + "esbuild-windows-arm64": "0.14.48" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.48.tgz", + "integrity": "sha512-3aMjboap/kqwCUpGWIjsk20TtxVoKck8/4Tu19rubh7t5Ra0Yrpg30Mt1QXXlipOazrEceGeWurXKeFJgkPOUg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.48.tgz", + "integrity": "sha512-vptI3K0wGALiDq+EvRuZotZrJqkYkN5282iAfcffjI5lmGG9G1ta/CIVauhY42MBXwEgDJkweiDcDMRLzBZC4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.48.tgz", + "integrity": "sha512-gGQZa4+hab2Va/Zww94YbshLuWteyKGD3+EsVon8EWTWhnHFRm5N9NbALNbwi/7hQ/hM1Zm4FuHg+k6BLsl5UA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.48.tgz", + "integrity": "sha512-bFjnNEXjhZT+IZ8RvRGNJthLWNHV5JkCtuOFOnjvo5pC0sk2/QVk0Qc06g2PV3J0TcU6kaPC3RN9yy9w2PSLEA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.48.tgz", + "integrity": "sha512-1NOlwRxmOsnPcWOGTB10JKAkYSb2nue0oM1AfHWunW/mv3wERfJmnYlGzL3UAOIUXZqW8GeA2mv+QGwq7DToqA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.48.tgz", + "integrity": "sha512-gXqKdO8wabVcYtluAbikDH2jhXp+Klq5oCD5qbVyUG6tFiGhrC9oczKq3vIrrtwcxDQqK6+HDYK8Zrd4bCA9Gw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.48.tgz", + "integrity": "sha512-ghGyDfS289z/LReZQUuuKq9KlTiTspxL8SITBFQFAFRA/IkIvDpnZnCAKTCjGXAmUqroMQfKJXMxyjJA69c/nQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.48.tgz", + "integrity": "sha512-vni3p/gppLMVZLghI7oMqbOZdGmLbbKR23XFARKnszCIBpEMEDxOMNIKPmMItQrmH/iJrL1z8Jt2nynY0bE1ug==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.48.tgz", + "integrity": "sha512-+VfSV7Akh1XUiDNXgqgY1cUP1i2vjI+BmlyXRfVz5AfV3jbpde8JTs5Q9sYgaoq5cWfuKfoZB/QkGOI+QcL1Tw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.48.tgz", + "integrity": "sha512-3CFsOlpoxlKPRevEHq8aAntgYGYkE1N9yRYAcPyng/p4Wyx0tPR5SBYsxLKcgPB9mR8chHEhtWYz6EZ+H199Zw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.48.tgz", + "integrity": "sha512-cs0uOiRlPp6ymknDnjajCgvDMSsLw5mST2UXh+ZIrXTj2Ifyf2aAP3Iw4DiqgnyYLV2O/v/yWBJx+WfmKEpNLA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.48.tgz", + "integrity": "sha512-+2F0vJMkuI0Wie/wcSPDCqXvSFEELH7Jubxb7mpWrA/4NpT+/byjxDz0gG6R1WJoeDefcrMfpBx4GFNN1JQorQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.48.tgz", + "integrity": "sha512-BmaK/GfEE+5F2/QDrIXteFGKnVHGxlnK9MjdVKMTfvtmudjY3k2t8NtlY4qemKSizc+QwyombGWTBDc76rxePA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.48.tgz", + "integrity": "sha512-tndw/0B9jiCL+KWKo0TSMaUm5UWBLsfCKVdbfMlb3d5LeV9WbijZ8Ordia8SAYv38VSJWOEt6eDCdOx8LqkC4g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.48.tgz", + "integrity": "sha512-V9hgXfwf/T901Lr1wkOfoevtyNkrxmMcRHyticybBUHookznipMOHoF41Al68QBsqBxnITCEpjjd4yAos7z9Tw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.48.tgz", + "integrity": "sha512-+IHf4JcbnnBl4T52egorXMatil/za0awqzg2Vy6FBgPcBpisDWT2sVz/tNdrK9kAqj+GZG/jZdrOkj7wsrNTKA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.48.tgz", + "integrity": "sha512-77m8bsr5wOpOWbGi9KSqDphcq6dFeJyun8TA+12JW/GAjyfTwVtOnN8DOt6DSPUfEV+ltVMNqtXUeTeMAxl5KA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.48.tgz", + "integrity": "sha512-EPgRuTPP8vK9maxpTGDe5lSoIBHGKO/AuxDncg5O3NkrPeLNdvvK8oywB0zGaAZXxYWfNNSHskvvDgmfVTguhg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.48.tgz", + "integrity": "sha512-YmpXjdT1q0b8ictSdGwH3M8VCoqPpK1/UArze3X199w6u8hUx3V8BhAi1WjbsfDYRBanVVtduAhh2sirImtAvA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.48.tgz", + "integrity": "sha512-HHaOMCsCXp0rz5BT2crTka6MPWVno121NKApsGs/OIW5QC0ggC69YMGs1aJct9/9FSUF4A1xNE/cLvgB5svR4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", + "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "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": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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-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-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", + "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "dependencies": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extract-stack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-2.0.0.tgz", + "integrity": "sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-merger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", + "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-node-info": "^2.1.0", + "fs-extra": "^8.0.1", + "fs-tree-diff": "^2.0.1", + "walk-sync": "^2.2.0" + } + }, + "node_modules/fs-merger/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-merger/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-merger/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fs-tree-diff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", + "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", + "dev": true, + "dependencies": { + "@types/symlink-or-copy": "^1.2.0", + "heimdalljs-logger": "^0.1.7", + "object-assign": "^4.1.0", + "path-posix": "^1.0.0", + "symlink-or-copy": "^1.1.8" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/git-parse": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/git-parse/-/git-parse-2.1.1.tgz", + "integrity": "sha512-jt4pYVdXFFjZMsFohMBYhPsW4j/XBDas7VctxBbRvKCyvQj5pk/+fe2/9Y1hykLSk3lHeVZyScDaW5cZBXDTyw==", + "dev": true, + "dependencies": { + "byline": "5.0.0", + "util.promisify": "1.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/git-rev-sync": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-3.0.2.tgz", + "integrity": "sha512-Nd5RiYpyncjLv0j6IONy0lGzAqdRXUaBctuGBbrEA2m6Bn4iDrN/9MeQTXuiquw8AEKL9D2BW0nw5m/lQvxqnQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "1.0.5", + "graceful-fs": "4.1.15", + "shelljs": "0.8.5" + } + }, + "node_modules/git-rev-sync/node_modules/graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "node_modules/git-up": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz", + "integrity": "sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==", + "dev": true, + "dependencies": { + "is-ssh": "^1.3.0", + "parse-url": "^6.0.0" + } + }, + "node_modules/git-url-parse": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.6.0.tgz", + "integrity": "sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g==", + "dev": true, + "dependencies": { + "git-up": "^4.0.0" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globule": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.3.tgz", + "integrity": "sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==", + "dev": true, + "dependencies": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/globule/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globule/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" + }, + "node_modules/gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha1-xnYqLx8N4KP8WVohWZ0/rI26Gso=", + "dev": true, + "dependencies": { + "through2": "^2.0.1" + } + }, + "node_modules/gulp-sort/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-sort/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gulp-sort/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "dev": true, + "dependencies": { + "rsvp": "~3.2.1" + } + }, + "node_modules/heimdalljs-logger": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", + "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "heimdalljs": "^0.2.6" + } + }, + "node_modules/heimdalljs-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/heimdalljs-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/heimdalljs/node_modules/rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha1-B8tKXfJa3Z6Cbrxn3Mn9idsn2Eo=", + "dev": true + }, + "node_modules/hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" + }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=" + }, + "node_modules/hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=" + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-call": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", + "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==", + "dev": true, + "dependencies": { + "content-type": "^1.0.4", + "debug": "^4.1.1", + "is-retry-allowed": "^1.1.0", + "is-stream": "^2.0.0", + "parse-json": "^4.0.0", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-call/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperlinker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", + "integrity": "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/i18next": { + "version": "21.8.13", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.13.tgz", + "integrity": "sha512-DpzwrJq7Y8tjUHxx6ByOkUIjrGYdQI5Mfv4XEI7q2RWdknQ7TaO9bKi8hS/LqYD6pBV5YGxJLReyLkOCxIpouA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.17.2" + } + }, + "node_modules/i18next-parser": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-8.13.0.tgz", + "integrity": "sha512-XU7resoeNcpJazh29OncQQUH6HsgCxk06RqBBDAmLHldafxopfCHY1vElyG/o3EY0Sn7XjelAmPTV0SgddJEww==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0-rc.2", + "colors": "1.4.0", + "commander": "~11.1.0", + "eol": "^0.9.1", + "esbuild": "^0.20.1", + "fs-extra": "^11.1.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1", + "js-yaml": "4.1.0", + "lilconfig": "^3.0.0", + "rsvp": "^4.8.2", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "~3.0.0", + "vinyl-fs": "^4.0.0", + "vue-template-compiler": "^2.6.11" + }, + "bin": { + "i18next": "bin/cli.js" + }, + "engines": { + "node": ">=16.0.0 || >=18.0.0 || >=20.0.0", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/i18next-parser/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/i18next-parser/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/i18next-parser/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/i18next-parser/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/i18next-parser/node_modules/i18next": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.1.tgz", + "integrity": "sha512-mXw4A24BiPZKRsbb9ewgSvjYd6fxFCNwJyfK6nYfSTIAX2GkCWcb598m3DFkDZmqADatvuASrKo6qwORz3VwTQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-parser/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/i18next-parser/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflected": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflected/-/inflected-2.1.0.tgz", + "integrity": "sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dependencies": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "dev": true, + "dependencies": { + "symbol-observable": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-observable/node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dev": true, + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", + "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/java-properties": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", + "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/jest-diff": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.1.tgz", + "integrity": "sha512-/MUUxeR2fHbqHoMMiffe/Afm+U8U4olFRJ0hiVG2lZatPJcnGxx292ustVu7bULhjV65IYMxRdploAKLbcrsyg==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", + "integrity": "sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==", + "dependencies": { + "@jest/schemas": "^28.0.2", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.1.tgz", + "integrity": "sha512-NPJPRWrbmR2nAJ+1nmnfcKKzSwgfaciCCrYZzVnNoxVoyusYWIjkBMNvu0RHJe7dNj4hH3uZOPZsQA+xAYWqsw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", + "integrity": "sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==", + "dependencies": { + "@jest/schemas": "^28.0.2", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/lint-staged": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", + "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.17", + "commander": "^9.3.0", + "debug": "^4.3.4", + "execa": "^6.1.0", + "lilconfig": "2.0.5", + "listr2": "^4.0.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-inspect": "^1.12.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.1", + "yaml": "^2.1.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", + "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", + "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", + "dev": true, + "dependencies": { + "@samverschueren/stream-to-observable": "^0.3.0", + "is-observable": "^1.1.0", + "is-promise": "^2.1.0", + "is-stream": "^1.1.0", + "listr-silent-renderer": "^1.1.1", + "listr-update-renderer": "^0.5.0", + "listr-verbose-renderer": "^0.5.0", + "p-map": "^2.0.0", + "rxjs": "^6.3.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/listr-update-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", + "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", + "log-symbols": "^1.0.2", + "log-update": "^2.3.0", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "listr": "^0.14.2" + } + }, + "node_modules/listr-update-renderer/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "dependencies": { + "slice-ansi": "0.0.4", + "string-width": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/listr-update-renderer/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr-update-renderer/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/listr-verbose-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", + "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "cli-cursor": "^2.1.0", + "date-fns": "^1.27.2", + "figures": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/listr-verbose-renderer/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/listr/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/listr2": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", + "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.5", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/listr2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/log-update/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/listr2/node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/rxjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", + "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/local-pkg": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.1.tgz", + "integrity": "sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.identity": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz", + "integrity": "sha1-rXvGpOZH15yXLhuA/u968VYmeHY=", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=" + }, + "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.xorby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.xorby/-/lodash.xorby-4.7.0.tgz", + "integrity": "sha512-gYiD6nvuQy0AEkMoUju+t4f4Rn18fjsLB/7x7YZFqtFT9kmegRLrj/uGEQVyVDy7otTmSrIMXNOk2wwuLcfHCQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "dependencies": { + "chalk": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/log-symbols/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/log-symbols/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/log-update": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "dev": true, + "dependencies": { + "ansi-escapes": "^3.0.0", + "cli-cursor": "^2.0.0", + "wrap-ansi": "^3.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "dev": true, + "dependencies": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, + "node_modules/mapbox-gl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.9.1.tgz", + "integrity": "sha512-PWEyI0FUeWaCbDnKmzHBi6CIB264SG6O894PJL1q4Cz/ZiHNyAROonCD48bxVWSQnXXAEDPyhJS5MiJ2K5DTjw==", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.1", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^2.0.1", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.5", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.3", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.2", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.4", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mktemp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", + "integrity": "sha1-bQUVYRyKjITkhKogABKbmOmB/ws=", + "dev": true, + "engines": { + "node": ">0.9" + } + }, + "node_modules/modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E=" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/natural-orderby": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz", + "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==" + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz", + "integrity": "sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==", + "dev": true, + "dependencies": { + "array.prototype.reduce": "^1.0.4", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optimism": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", + "integrity": "sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==", + "dependencies": { + "@wry/context": "^0.6.0", + "@wry/trie": "^0.3.0" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-path": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.4.tgz", + "integrity": "sha512-Z2lWUis7jlmXC1jeOG9giRO2+FsuyNipeQ43HAjqAZjwSe3SEf+q/84FGPHoso3kyntbxa4c4i77t3m6fGf8cw==", + "dev": true, + "dependencies": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0", + "qs": "^6.9.4", + "query-string": "^6.13.8" + } + }, + "node_modules/parse-path/node_modules/protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "dev": true + }, + "node_modules/parse-url": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-6.0.5.tgz", + "integrity": "sha512-e35AeLTSIlkw/5GFq70IN7po8fmDUjpDPY1rIK+VubRfsUvBonjQ+PBZG+vWMACnQSmNlvl524IucoDmcioMxA==", + "dev": true, + "dependencies": { + "is-ssh": "^1.3.0", + "normalize-url": "^6.1.0", + "parse-path": "^4.0.0", + "protocols": "^1.4.0" + } + }, + "node_modules/parse-url/node_modules/protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "dev": true + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/password-prompt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.2.tgz", + "integrity": "sha512-bpuBhROdrhuN3E7G/koAju0WjVw9/uQOG5Co5mokNj0MiOSBVZS1JTwM4zl55hu0WFmIEFvO9cU9sJQiBIYeIA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^3.1.0", + "cross-spawn": "^6.0.5" + } + }, + "node_modules/password-prompt/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/password-prompt/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/password-prompt/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/password-prompt/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/password-prompt/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/password-prompt/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/password-prompt/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-functions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", + "integrity": "sha512-N5yWXWKA+uhpLQ9ZhBRl2bIAdM6oVJYpDojuI1nF2SzXBimJcdjFwiAouBVbO5VuOF3qA6BSFWFc3wXbbj72XQ==", + "dependencies": { + "glob": "^7.1.2", + "object-assign": "^4.1.1", + "postcss": "^6.0.9", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/postcss-functions/node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-functions/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, + "node_modules/postcss-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-2.0.3.tgz", + "integrity": "sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==", + "dependencies": { + "camelcase-css": "^2.0.1", + "postcss": "^7.0.18" + } + }, + "node_modules/postcss-js/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "node_modules/postcss-js/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-nested": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-4.2.3.tgz", + "integrity": "sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw==", + "dependencies": { + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2" + } + }, + "node_modules/postcss-nested/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "node_modules/postcss-nested/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-map-series": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", + "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", + "dev": true, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, + "node_modules/protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/purgecss": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", + "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", + "dependencies": { + "commander": "^8.0.0", + "glob": "^7.1.7", + "postcss": "^8.3.5", + "postcss-selector-parser": "^6.0.6" + }, + "bin": { + "purgecss": "bin/purgecss.js" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", + "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", + "dev": true, + "dependencies": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quick-temp": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", + "integrity": "sha1-urAqJCq4+w3XWKPJd2sy+aXZRAg=", + "dev": true, + "dependencies": { + "mktemp": "~0.4.0", + "rimraf": "^2.5.4", + "underscore.string": "~3.3.4" + } + }, + "node_modules/quick-temp/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-blurhash": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.1.3.tgz", + "integrity": "sha512-Q9lqbXg92NU6/2DoIl/cBM8YWL+Z4X66OiG4aT9ozOgjBwx104LHFCH5stf6aF+s0Q9Wf310Ul+dG+VXJltmPg==", + "peerDependencies": { + "blurhash": "^1.1.1", + "react": ">=15" + } + }, + "node_modules/react-datepicker": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.0.0.tgz", + "integrity": "sha512-LGzKgBk5NUEcXUeSEJY2ICCCmPusm9UGRkNKRXCPgpUzoMx2CCsa0mOHTyv+itQeW7tR/jgGEpq5q/afZjGEFg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "date-fns-tz": "^3.0.0", + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "date-fns-tz": { + "optional": true + } + } + }, + "node_modules/react-datepicker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.33.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.33.1.tgz", + "integrity": "sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-i18next": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.0.tgz", + "integrity": "sha512-coJujU20xJ5Wa5rHjTyB5LFKZb1yfXo2A+40RRSyAF0FlZRHyy+3C1Mr92x1JPfS7W7v2TWNn8mRhpDFGJwXVg==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "dependencies": { + "history": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", + "dependencies": { + "history": "^5.2.0", + "react-router": "6.3.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-swipeable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.0.tgz", + "integrity": "sha512-NI7KGfQ6gwNFN0Hor3vytYW3iRfMMaivGEuxcADOOfBCx/kqwXE8IfHFxEcxSUkxCYf38COLKYd9EMYZghqaUA==", + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + } + }, + "node_modules/react-test-renderer": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", + "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", + "dependencies": { + "react-is": "^18.2.0", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.1.tgz", + "integrity": "sha512-PF61BHLaOGF5oIKTpSrDM6Qfy2d7DIx5qblgqG+wjqHuFH97OgAqBYFIJwEuHTrM6pQGT17IJ8D0C/jVu/0tig==", + "dev": true, + "dependencies": { + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "dev": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dependencies": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=" + }, + "node_modules/rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/roarr/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, + "node_modules/rollup": { + "version": "2.66.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.66.1.tgz", + "integrity": "sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "engines": { + "node": "6.* || >= 7.*" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sirv": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.2.tgz", + "integrity": "sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", + "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dev": true, + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "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.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-components": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz", + "integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==", + "hasInstallScript": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/subscriptions-transport-ws": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz", + "integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==", + "dependencies": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependencies": { + "graphql": "^15.7.2 || ^16.0.0" + } + }, + "node_modules/subscriptions-transport-ws/node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supercluster": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz", + "integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==", + "dependencies": { + "kdbush": "^3.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "dev": true + }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" + }, + "node_modules/table": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", + "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz", + "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/tailwind-override": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/tailwind-override/-/tailwind-override-0.6.1.tgz", + "integrity": "sha512-FOxLFcXTe9eqC85SNCREyWw/CG8hyNSBJNrp+rjMDV7m63UoJzpkNJtXNrA0y3tE9m0d3ccNj8HBVQG/hfmNxA==" + }, + "node_modules/tailwindcss": { + "name": "@tailwindcss/postcss7-compat", + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.17.tgz", + "integrity": "sha512-3h2svqQAqYHxRZ1KjsJjZOVTQ04m29LjfrLjXyZZEJuvUuJN+BCIF9GI8vhE1s0plS0mogd6E6YLg6mu4Wv/Vw==", + "dependencies": { + "arg": "^5.0.1", + "autoprefixer": "^9", + "bytes": "^3.0.0", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "color": "^4.0.1", + "cosmiconfig": "^7.0.1", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.7", + "fs-extra": "^10.0.0", + "glob-parent": "^6.0.1", + "html-tags": "^3.1.0", + "is-color-stop": "^1.1.0", + "is-glob": "^4.0.1", + "lodash": "^4.17.21", + "lodash.topath": "^4.5.2", + "modern-normalize": "^1.1.0", + "node-emoji": "^1.11.0", + "normalize-path": "^3.0.0", + "object-hash": "^2.2.0", + "postcss": "^7", + "postcss-functions": "^3", + "postcss-js": "^2", + "postcss-load-config": "^3.1.0", + "postcss-nested": "^4", + "postcss-selector-parser": "^6.0.6", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", + "quick-lru": "^5.1.1", + "reduce-css-calc": "^2.1.8", + "resolve": "^1.20.0", + "tmp": "^0.2.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/tailwindcss/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/tailwindcss/node_modules/autoprefixer": { + "version": "9.8.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", + "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", + "dependencies": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "picocolors": "^0.2.1", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + }, + "node_modules/tailwindcss/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/tailwindcss/node_modules/color": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.0.tgz", + "integrity": "sha512-hHTcrbvEnGjC7WBMk6ibQWFVDgEFTVmjrz2Q5HlU6ltwxv0JJN2Z8I7uRbWeQLF04dikxs8zgyZkazRJvSMtyQ==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, + "node_modules/tailwindcss/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/tailwindcss/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/tailwindcss/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "node_modules/tailwindcss/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.1.tgz", + "integrity": "sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg==", + "dependencies": { + "lilconfig": "^2.0.4", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.2.2.tgz", + "integrity": "sha512-tp4n5OARNL3v8ntdJUyo5NsDfwvUtu8isB43USjrsQxQrADDKY6UGBkmFaw/2vNmEt8S/uSm2U5FhkiK1eAFGw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, + "node_modules/tinyspy": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-0.3.3.tgz", + "integrity": "sha512-gRiUR8fuhUf0W9lzojPf1N1euJYA30ISebSfgca8z76FOvXtVXqd5ojEIaKLWbDQhAaC3ibxZIjqbyi4ybjcTw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, + "node_modules/totalist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", + "integrity": "sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tty": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tty/-/tty-1.0.1.tgz", + "integrity": "sha1-5ECayYsN0cULWf846G6sPwdk7kU=", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/underscore.string/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/util.promisify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.1.tgz", + "integrity": "sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "for-each": "^0.3.3", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/vite": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.18.tgz", + "integrity": "sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==", + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/vite-plugin-svgr": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.2.0.tgz", + "integrity": "sha512-CRJgz92vf3SQbt3jwF6/BhyQIhbY0pwbXAPqUh1/EMG6a4eHsHCECcDNrKZMhlRBj3LyXQSfHXJLtM2ISP5rfg==", + "dependencies": { + "@rollup/pluginutils": "^4.2.1", + "@svgr/core": "^6.2.1" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.0.0.tgz", + "integrity": "sha512-MdPdhdWLtQsjd29Wa4pABdhWbaRMACdM1h31BY+c6FghTZqNGT7pEYdBoaGeKtdTOBC/XNFQaKVj+r/Ei2ryWA==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.0.0.tgz", + "integrity": "sha512-aVdtfx9jlaaxc3unA6l+M9YRnKIZjOhQPthLKqmTXC8UVkBLDRGwPKo+r8n3VZN8B34+yVajzPTZ+ptTSuZZCw==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.0.0.tgz", + "integrity": "sha512-Ccj42ApsePD451AZJJf1QzTD1B/BOU392URJTeXFxSK709i0KUsGtbwyiqsKu7vsYxpTM0IA5clAKDyf9RCZyA==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.0.0.tgz", + "integrity": "sha512-88V26WGyt1Sfd1emBYmBJRWMmgarrExpKNVmI9vVozha4kqs6FzQJ/Kp5+EYli1apgX44518/0+t9+NU36lThQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.0.0.tgz", + "integrity": "sha512-F7YXNLfGze+xv0KMQxrl2vkNbI9kzT9oDK55/kUuymh1ACyXkMV+VZWX1zEhSTfEKh7VkHVZGmVtHg8eTZ6PRg==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.0.0.tgz", + "integrity": "sha512-+rghFXxdIqJNLQK08kwPBD3Z22/0b2tEZ9lKiL/yTfuyj1wW8HUXu4bo/XkogATIYuXSghVQOOCwURXzHGKyZA==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.0.0.tgz", + "integrity": "sha512-VaphyHZ+xIKv5v0K0HCzyfAaLhPGJXSk2HkpYfXIOKb7DjLBv0soHDxNv6X0vr2titsxE7klb++u7iOf7TSrFQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.2.0.tgz", + "integrity": "sha512-bhYIpsORb++wpsp91fymbFkf09Z/YEKR0DnFjxvN+8JHeCUD2unnh18jIMKnDJTWtvpTaGYPXELVe4OOzFI0xg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/babel-preset": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.2.0.tgz", + "integrity": "sha512-4WQNY0J71JIaL03DRn0vLiz87JXx0b9dYm2aA8XHlQJQoixMl4r/soYHm8dsaJZ3jWtkCiOYy48dp9izvXhDkQ==", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^6.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^6.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^6.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "^6.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "^6.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "^6.0.0", + "@svgr/babel-plugin-transform-svg-component": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/core": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.2.1.tgz", + "integrity": "sha512-NWufjGI2WUyrg46mKuySfviEJ6IxHUOm/8a3Ph38VCWSp+83HBraCQrpEM3F3dB6LBs5x8OElS8h3C0oOJaJAA==", + "dependencies": { + "@svgr/plugin-jsx": "^6.2.1", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/hast-util-to-babel-ast": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.2.1.tgz", + "integrity": "sha512-pt7MMkQFDlWJVy9ULJ1h+hZBDGFfSCwlBNW1HkLnVi7jUhyEXUaGYWi1x6bM2IXuAR9l265khBT4Av4lPmaNLQ==", + "dependencies": { + "@babel/types": "^7.15.6", + "entities": "^3.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@svgr/plugin-jsx": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.2.1.tgz", + "integrity": "sha512-u+MpjTsLaKo6r3pHeeSVsh9hmGRag2L7VzApWIaS8imNguqoUwDq/u6U/NDmYs/KAsrmtBjOEaAAPbwNGXXp1g==", + "dependencies": { + "@babel/core": "^7.15.5", + "@svgr/babel-preset": "^6.2.0", + "@svgr/hast-util-to-babel-ast": "^6.2.1", + "svg-parser": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "^6.0.0" + } + }, + "node_modules/vite-plugin-svgr/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/vite-plugin-svgr/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/vitest": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.17.1.tgz", + "integrity": "sha512-d6NsFC6FPmZ5XdiSYfW5rwJ/b8060wqe2steNNlVbhO69HWma6CucIm5g7PXlCSkmKvrdEbUsZHPAarlH83VGw==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.1", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.1", + "tinypool": "^0.2.1", + "tinyspy": "^0.3.3", + "vite": "^2.9.12 || ^3.0.0-0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/ui": "*", + "c8": "*", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "c8": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "dev": true, + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dev": true, + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dev": true, + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz", + "integrity": "sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg==", + "dev": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "dev": true + }, + "node_modules/vscode-uri": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.6.tgz", + "integrity": "sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==", + "dev": true + }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/whatwg-url/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "license": "MIT", + "dependencies": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yarn": { + "version": "1.22.19", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", + "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "yarn": "bin/yarn.js", + "yarnpkg": "bin/yarn.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "node_modules/zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "dependencies": { + "zen-observable": "0.8.15" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@apollo/client": { + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz", + "integrity": "sha512-Y1yu8qa2YeaCUBVuw08x8NHenFi0sw2I3KCu7Kw9mDSu86HmmtHJkCAifKVrN2iPgDTW/BbP3EpSV8/EQCcxZA==", + "requires": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/context": "^0.6.0", + "@wry/equality": "^0.5.0", + "@wry/trie": "^0.3.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.16.1", + "prop-types": "^15.7.2", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + } + }, + "@apollo/federation": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.27.0.tgz", + "integrity": "sha512-hMeRN9IPsIn+5J5SmWof0ODbvRjRj8mBNqbsm9Zjkqjbw6RTlcx90taMk7cYhcd/E+uTyLQt5cOSRVBx53cxbQ==", + "dev": true, + "requires": { + "apollo-graphql": "^0.9.3", + "lodash.xorby": "^4.7.0" + } + }, + "@apollo/utils.keyvaluecache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.1.tgz", + "integrity": "sha512-nLgYLomqjVimEzQ4cdvVQkcryi970NDvcRVPfd0OPeXhBfda38WjBq+WhQFk+czSHrmrSp34YHBxpat0EtiowA==", + "dev": true, + "requires": { + "@apollo/utils.logger": "^1.0.0", + "lru-cache": "^7.10.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.12.0.tgz", + "integrity": "sha512-OIP3DwzRZDfLg9B9VP/huWBlpvbkmbfiBy8xmsXp4RPmE4A3MhwNozc5ZJ3fWnSg8fDcdlE/neRTPG2ycEKliw==", + "dev": true + } + } + }, + "@apollo/utils.logger": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-1.0.0.tgz", + "integrity": "sha512-dx9XrjyisD2pOa+KsB5RcDbWIAdgC91gJfeyLCgy0ctJMjQe7yZK5kdWaWlaOoCeX0z6YI9iYlg7vMPyMpQF3Q==", + "dev": true + }, + "@apollographql/apollo-tools": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.4.tgz", + "integrity": "sha512-shM3q7rUbNyXVVRkQJQseXv6bnYM3BUma/eZhwXR4xsuM+bqWnJKvW7SAfRjP7LuSCocrexa5AXhjjawNHrIlw==", + "dev": true, + "requires": {} + }, + "@apollographql/graphql-language-service-interface": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-interface/-/graphql-language-service-interface-2.0.2.tgz", + "integrity": "sha512-28wePK0hlIVjgmvMXMAUq8qRSjz9O+6lqFp4PzOTHtfJfSsjVe9EfjF98zTpHsTgT3HcOxmbqDZZy8jlXtOqEA==", + "dev": true, + "requires": { + "@apollographql/graphql-language-service-parser": "^2.0.0", + "@apollographql/graphql-language-service-types": "^2.0.0", + "@apollographql/graphql-language-service-utils": "^2.0.2" + } + }, + "@apollographql/graphql-language-service-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-parser/-/graphql-language-service-parser-2.0.2.tgz", + "integrity": "sha512-rpTPrEJu1PMaRQxz5P8BZWsixNNhYloS0H0dwTxNBuE3qctbARvR7o8UCKLsmKgTbo+cz3T3a6IAsWlkHgMWGg==", + "dev": true, + "requires": { + "@apollographql/graphql-language-service-types": "^2.0.0" + } + }, + "@apollographql/graphql-language-service-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-types/-/graphql-language-service-types-2.0.2.tgz", + "integrity": "sha512-vE+Dz8pG+Xa1Z2nMl82LoO66lQ6JqBUjaXqLDvS3eMjvA3N4hf+YUDOWfPdNZ0zjhHhHXzUIIZCkax6bXfFbzQ==", + "dev": true, + "requires": {} + }, + "@apollographql/graphql-language-service-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-language-service-utils/-/graphql-language-service-utils-2.0.2.tgz", + "integrity": "sha512-fDj5rWlTi/czvUS5t7V7I45Ai6bOO3Z7JARYj21Y2xxfbRGtJi6h8FvLX0N/EbzQgo/fiZc/HAhtfwn+OCjD7A==", + "dev": true, + "requires": { + "@apollographql/graphql-language-service-types": "^2.0.0" + } + }, + "@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "requires": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.6.tgz", + "integrity": "sha512-tzulrgDT0QD6U7BJ4TKVk2SDDg7wlP39P9yAx1RfLy7vP/7rsDRlWVfbWxElslu56+r7QOhB2NSDsabYYruoZQ==" + }, + "@babel/core": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.6.tgz", + "integrity": "sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==", + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.6", + "@babel/helper-compilation-targets": "^7.18.6", + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helpers": "^7.18.6", + "@babel/parser": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "requires": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz", + "integrity": "sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg==", + "requires": { + "@babel/compat-data": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.6.tgz", + "integrity": "sha512-YfDzdnoxHGV8CzqHGyCbFvXg5QESPFkXlHtvdCkesLjjVMT2Adxe4FGUR5ChIb3DxSaXO12iIOCWoXdsUVwnqw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.6", + "@babel/helper-function-name": "^7.18.6", + "@babel/helper-member-expression-to-functions": "^7.18.6", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.6.tgz", + "integrity": "sha512-CeHxqwwipekotzPDUuJOfIMtcIHBuc7WAzLmTYWctVigqS5RktNMQ5bEwQSuGewzYnCtTWa3BARXeiLxDTv+Ng==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.6.tgz", + "integrity": "sha512-L//phhB4al5uucwzlimruukHB3jRd5JGClwRMD/ROrVjXfLqovYnvQrK/JK36WYyVwGGO7OD3kMyVTjx+WVPhw==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz", + "integrity": "sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==" + }, + "@babel/helper-replace-supers": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.18.6.tgz", + "integrity": "sha512-fTf7zoXnUGl9gF25fXCWE26t7Tvtyn6H4hkLSYhATwJvw2uYxd3aoXplMSe0g9XbwK7bmxNes7+FGO0rB/xC0g==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.6", + "@babel/helper-member-expression-to-functions": "^7.18.6", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==" + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==" + }, + "@babel/helpers": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.6.tgz", + "integrity": "sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==", + "requires": { + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.6", + "@babel/types": "^7.18.6" + } + }, + "@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==" + }, + "@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", + "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz", + "integrity": "sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.18.6" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", + "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "requires": { + "@babel/plugin-transform-react-jsx": "^7.18.6" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz", + "integrity": "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz", + "integrity": "sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.6.tgz", + "integrity": "sha512-ijHNhzIrLj5lQCnI6aaNVRtGVuUZhOXFLRVFs7lLrkXTHip4FKty5oAuQdk4tywG0/WjXmjTfQCWmuzrvFer1w==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-typescript": "^7.18.6" + } + }, + "@babel/preset-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", + "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-typescript": "^7.18.6" + } + }, + "@babel/runtime": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + } + }, + "@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "requires": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@emotion/is-prop-valid": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.3.tgz", + "integrity": "sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA==", + "requires": { + "@emotion/memoize": "^0.7.4" + } + }, + "@emotion/memoize": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz", + "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "@endemolshinegroup/cosmiconfig-typescript-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", + "integrity": "sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==", + "dev": true, + "requires": { + "lodash.get": "^4", + "make-error": "^1", + "ts-node": "^9", + "tslib": "^2" + }, + "dependencies": { + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + } + } + }, + "@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "dev": true, + "optional": true + }, + "@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "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 + }, + "globals": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", + "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "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" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "requires": { + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "requires": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "requires": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + } + }, + "@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "requires": { + "@floating-ui/dom": "^1.7.4" + } + }, + "@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "@graphql-typed-document-node/core": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "requires": {} + }, + "@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "requires": { + "is-negated-glob": "^1.0.0" + } + }, + "@headlessui/react": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.6.tgz", + "integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==", + "requires": {} + }, + "@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "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/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/schemas": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.0.2.tgz", + "integrity": "sha512-YVDJZjd4izeTDkij00vHHAymNXQ6WWsdChFRK86qck6Jpr3DCL5W3Is3vslviRlP+bLuMYRLbdp98amMvqudhA==", + "requires": { + "@sinclair/typebox": "^0.23.3" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@mapbox/geojson-rewind": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.1.tgz", + "integrity": "sha512-eL7fMmfTBKjrb+VFHXCGv9Ot0zc3C0U+CwXo1IrP+EPwDczLoXv34Tgq3y+2mPSFNVUXgU42ILWJTC7145KPTA==", + "requires": { + "get-stream": "^6.0.1", + "minimist": "^1.2.5" + } + }, + "@mapbox/geojson-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", + "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==" + }, + "@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha1-zlblOfg1UrWNENZy6k1vya3HsjQ=" + }, + "@mapbox/mapbox-gl-supported": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" + }, + "@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI=" + }, + "@mapbox/tiny-sdf": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz", + "integrity": "sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw==" + }, + "@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha1-FWUb1VOme4WB+zmIEMmK2Go0Uk4=" + }, + "@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "requires": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@oclif/color": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@oclif/color/-/color-1.0.1.tgz", + "integrity": "sha512-qjYr+izgWdIVOroiBKqTzQgc1r5Wd9QB1J7yGM2EeelqhBARiiVLRZL45vhV4zdyTRdDkZS0EBzFwQap+nliLA==", + "dev": true, + "requires": { + "ansi-styles": "^4.2.1", + "chalk": "^4.1.0", + "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "tslib": "^2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@oclif/command": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.16.tgz", + "integrity": "sha512-rmVKYEsKzurfRU0xJz+iHelbi1LGlihIWZ7Qvmb/CBz1EkhL7nOkW4SVXmG2dA5Ce0si2gr88i6q4eBOMRNJ1w==", + "dev": true, + "requires": { + "@oclif/config": "^1.18.2", + "@oclif/errors": "^1.3.5", + "@oclif/help": "^1.0.1", + "@oclif/parser": "^3.8.6", + "debug": "^4.1.1", + "semver": "^7.3.2" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@oclif/config": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.18.3.tgz", + "integrity": "sha512-sBpko86IrTscc39EvHUhL+c++81BVTsIZ3ETu/vG+cCdi0N6vb2DoahR67A9FI2CGnxRRHjnTfa3m6LulwNATA==", + "dev": true, + "requires": { + "@oclif/errors": "^1.3.5", + "@oclif/parser": "^3.8.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-wsl": "^2.1.1", + "tslib": "^2.3.1" + } + }, + "@oclif/core": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.9.5.tgz", + "integrity": "sha512-C605Cr4RhHTMXYApLxdVt/PL6IA5cguN6MPvsMjxkvBppk2Fvcsj05dtRdDqShskRpZzHtu65emG1tHy8TWPWQ==", + "dev": true, + "requires": { + "@oclif/linewrap": "^1.0.0", + "@oclif/screen": "^3.0.2", + "ansi-escapes": "^4.3.2", + "ansi-styles": "^4.3.0", + "cardinal": "^2.1.1", + "chalk": "^4.1.2", + "clean-stack": "^3.0.1", + "cli-progress": "^3.10.0", + "debug": "^4.3.4", + "ejs": "^3.1.6", + "fs-extra": "^9.1.0", + "get-package-type": "^0.1.0", + "globby": "^11.1.0", + "hyperlinker": "^1.0.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "js-yaml": "^3.14.1", + "natural-orderby": "^2.0.3", + "object-treeify": "^1.1.33", + "password-prompt": "^1.1.2", + "semver": "^7.3.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "supports-hyperlinks": "^2.2.0", + "tslib": "^2.3.1", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@oclif/errors": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.5.tgz", + "integrity": "sha512-OivucXPH/eLLlOT7FkCMoZXiaVYf8I/w1eTAM1+gKzfhALwWTusxEx7wBmW0uzvkSg/9ovWLycPaBgJbM3LOCQ==", + "dev": true, + "requires": { + "clean-stack": "^3.0.0", + "fs-extra": "^8.1", + "indent-string": "^4.0.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "@oclif/help": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@oclif/help/-/help-1.0.1.tgz", + "integrity": "sha512-8rsl4RHL5+vBUAKBL6PFI3mj58hjPCp2VYyXD4TAa7IMStikFfOH2gtWmqLzIlxAED2EpD0dfYwo9JJxYsH7Aw==", + "dev": true, + "requires": { + "@oclif/config": "1.18.2", + "@oclif/errors": "1.3.5", + "chalk": "^4.1.2", + "indent-string": "^4.0.0", + "lodash": "^4.17.21", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "@oclif/config": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.18.2.tgz", + "integrity": "sha512-cE3qfHWv8hGRCP31j7fIS7BfCflm/BNZ2HNqHexH+fDrdF2f1D5S8VmXWLC77ffv3oDvWyvE9AZeR0RfmHCCaA==", + "dev": true, + "requires": { + "@oclif/errors": "^1.3.3", + "@oclif/parser": "^3.8.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-wsl": "^2.1.1", + "tslib": "^2.0.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "@oclif/linewrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oclif/linewrap/-/linewrap-1.0.0.tgz", + "integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw==", + "dev": true + }, + "@oclif/parser": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.7.tgz", + "integrity": "sha512-b11xBmIUK+LuuwVGJpFs4LwQN2xj2cBWj2c4z1FtiXGrJ85h9xV6q+k136Hw0tGg1jQoRXuvuBnqQ7es7vO9/Q==", + "dev": true, + "requires": { + "@oclif/errors": "^1.3.5", + "@oclif/linewrap": "^1.0.0", + "chalk": "^4.1.0", + "tslib": "^2.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@oclif/plugin-autocomplete": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-1.3.0.tgz", + "integrity": "sha512-N2DRWKvuSXTGuaYf4buRbRfh5yNybb1cjQmPl9viY0BIqTwZgtQdzSD6ZSOkwda51RbGcQomYcc/h8T+ZFAkMQ==", + "dev": true, + "requires": { + "@oclif/core": "^1.7.0", + "chalk": "^4.1.0", + "debug": "^4.3.4", + "fs-extra": "^9.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@oclif/plugin-help": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-5.1.12.tgz", + "integrity": "sha512-HvH/RubJxqCinP0vUWQLTOboT+SfjfL8h40s+PymkWaldIcXlpoRaJX50vz+SjZIs7uewZwEk8fzLqpF/BWXlg==", + "dev": true, + "requires": { + "@oclif/core": "^1.3.6" + } + }, + "@oclif/plugin-not-found": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-2.3.1.tgz", + "integrity": "sha512-AeNBw+zSkRpePmpXO8xlL072VF2/R2yK3qsVs/JF26Yw1w77TWuRTdFR+hFotJtFCJ4QYqhNtKSjdryCO9AXsA==", + "dev": true, + "requires": { + "@oclif/color": "^1.0.0", + "@oclif/core": "^1.2.1", + "fast-levenshtein": "^3.0.0", + "lodash": "^4.17.21" + }, + "dependencies": { + "fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "requires": { + "fastest-levenshtein": "^1.0.7" + } + } + } + }, + "@oclif/plugin-plugins": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-2.1.0.tgz", + "integrity": "sha512-Bgt+QpTlX7+Q0HkVgtbUGYQlo/hyzNBAaXH5l16ou9Ji5wfi5T+niV5AzQ14R7JF8ZDOTbUOU/NRBJ2bzLCaZQ==", + "dev": true, + "requires": { + "@oclif/color": "^1.0.1", + "@oclif/core": "^1.2.0", + "chalk": "^4.1.2", + "debug": "^4.1.0", + "fs-extra": "^9.0", + "http-call": "^5.2.2", + "load-json-file": "^5.3.0", + "npm-run-path": "^4.0.1", + "semver": "^7.3.2", + "tslib": "^2.0.0", + "yarn": "^1.22.17" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@oclif/plugin-warn-if-update-available": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.0.4.tgz", + "integrity": "sha512-9dprC1CWPjesg0Vf/rDSQH2tzJXhP1ow84cb2My1kj6e6ESulPKpctiCFSZ1WaCQFfq+crKhzlNoP/vRaXNUAg==", + "dev": true, + "requires": { + "@oclif/core": "^1.0.8", + "chalk": "^4.1.0", + "debug": "^4.1.0", + "fs-extra": "^9.0.1", + "http-call": "^5.2.2", + "lodash": "^4.17.21", + "semver": "^7.3.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@oclif/screen": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-3.0.2.tgz", + "integrity": "sha512-S/SF/XYJeevwIgHFmVDAFRUvM3m+OjhvCAYMk78ZJQCYCQ5wS7j+LTt1ZEv2jpEEGg2tx/F6TYYWxddNAYHrFQ==", + "dev": true + }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "@samverschueren/stream-to-observable": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", + "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", + "dev": true, + "requires": { + "any-observable": "^0.3.0" + } + }, + "@sinclair/typebox": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.23.5.tgz", + "integrity": "sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg==" + }, + "@testing-library/dom": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz", + "integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/jest-dom": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz", + "integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/react": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.3.0.tgz", + "integrity": "sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" + } + }, + "@testing-library/user-event": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.2.1.tgz", + "integrity": "sha512-HOr1QiODrq+0j9lKU5i10y9TbhxMBMRMGimNx10asdmau9cb8Xb1Vyg0GvTwyIL2ziQyh2kAloOtAQFBQVuecA==", + "dev": true, + "requires": {} + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "devOptional": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "devOptional": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "devOptional": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "devOptional": true + }, + "@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, + "@types/chai": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", + "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", + "dev": true + }, + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" + }, + "@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/jest": { + "version": "28.1.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.4.tgz", + "integrity": "sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==", + "requires": { + "jest-matcher-utils": "^28.0.0", + "pretty-format": "^28.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "pretty-format": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", + "integrity": "sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==", + "requires": { + "@jest/schemas": "^28.0.2", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==" + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==" + }, + "@types/mapbox-gl": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.3.tgz", + "integrity": "sha512-XdveeJptNNZw7ZoeiAJ2/dupNtWaV6qpBG/SOFEpQNQAc+oiO6qUznX85n+W1XbLeD8SVRVfVORKuR+I4CHDZw==", + "requires": { + "@types/geojson": "*" + } + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "devOptional": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" + }, + "@types/react": { + "version": "18.0.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", + "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", + "integrity": "sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-helmet": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.5.tgz", + "integrity": "sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-router": { + "version": "5.1.18", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz", + "integrity": "sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==", + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "@types/styled-components": { + "version": "5.1.25", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.25.tgz", + "integrity": "sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==", + "requires": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "@types/symlink-or-copy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz", + "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", + "dev": true + }, + "@types/testing-library__jest-dom": { + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz", + "integrity": "sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg==", + "dev": true, + "requires": { + "@types/jest": "*" + } + }, + "@types/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-wDXw9LEEUHyV+7UWy7U315nrJGJ7p1BzaCxDpEoLr789Dk1WDVMMlf3iBfbG2F8NdWnYyFbtTxUn2ZNbm1Q4LQ==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", + "integrity": "sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/type-utils": "5.30.6", + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + } + }, + "@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz", + "integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.6.tgz", + "integrity": "sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "debug": "^4.3.4" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + } + }, + "@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/type-utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz", + "integrity": "sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + } + }, + "@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz", + "integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "requires": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==" + } + } + }, + "@vitest/ui": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.17.1.tgz", + "integrity": "sha512-B4PGDk5IZ10HT9GzR0NQ96VEphWY9dTf1yqBGNjPk2c7wQnhZJdHzv3tgFQiyFK5YR1cjtOV918QHCnptV4r5w==", + "dev": true, + "requires": { + "sirv": "^2.0.2" + } + }, + "@wry/context": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", + "integrity": "sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@wry/equality": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.2.tgz", + "integrity": "sha512-oVMxbUXL48EV/C0/M7gLVsoK6qRHPS85x8zECofEZOVvxGmIPLA9o5Z27cc2PoAyZz1S2VoM2A7FLAnpfGlneA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@wry/trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.1.tgz", + "integrity": "sha512-WwB53ikYudh9pIorgxrkHKrQZcCqNM/Q/bDzZBffEaGUKGuHrRb3zZUT9Sh2qw9yogC7SsdRmQ1ER0pqvd3bfw==", + "requires": { + "tslib": "^2.3.0" + } + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "devOptional": true + }, + "acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "requires": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + }, + "dependencies": { + "acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "requires": { + "acorn": "^8.11.0" + } + } + } + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + } + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, + "any-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", + "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "apollo": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/apollo/-/apollo-2.34.0.tgz", + "integrity": "sha512-gDH+WBN+b6TA/tIrIuAyO6Df4tsHwAA/t3NZqUitOM0gKo/nXNOUZzskAFTjErL6fgp5+kYIP3rZ+bIleqXAKg==", + "dev": true, + "requires": { + "@apollographql/apollo-tools": "0.5.4", + "@oclif/command": "1.8.16", + "@oclif/config": "1.18.3", + "@oclif/errors": "1.3.5", + "@oclif/plugin-autocomplete": "1.3.0", + "@oclif/plugin-help": "5.1.12", + "@oclif/plugin-not-found": "2.3.1", + "@oclif/plugin-plugins": "2.1.0", + "@oclif/plugin-warn-if-update-available": "2.0.4", + "apollo-codegen-core": "0.40.9", + "apollo-codegen-flow": "0.38.9", + "apollo-codegen-scala": "0.39.9", + "apollo-codegen-swift": "0.40.9", + "apollo-codegen-typescript": "0.40.9", + "apollo-env": "0.10.2", + "apollo-graphql": "0.9.7", + "apollo-language-server": "1.26.9", + "chalk": "4.1.2", + "cli-ux": "6.0.9", + "env-ci": "7.1.0", + "gaze": "1.1.3", + "git-parse": "2.1.1", + "git-rev-sync": "3.0.2", + "git-url-parse": "11.6.0", + "glob": "8.0.1", + "global-agent": "3.0.0", + "graphql": "^15.5.0", + "graphql-tag": "2.12.6", + "listr": "0.14.3", + "lodash.identity": "3.0.0", + "lodash.pickby": "4.6.0", + "mkdirp": "1.0.4", + "moment": "2.29.3", + "strip-ansi": "6.0.1", + "table": "6.8.0", + "tty": "1.0.1", + "vscode-uri": "1.0.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "glob": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz", + "integrity": "sha512-cF7FYZZ47YzmCu7dDy50xSRRfO3ErRfrXuLZcNIuyiJEco0XSrGtuilG19L5xp3NcwTx7Gn+X6Tv3fmsUPTbow==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "apollo-codegen-core": { + "version": "0.40.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-core/-/apollo-codegen-core-0.40.9.tgz", + "integrity": "sha512-AiynL9PWGZ9zXq9gbJENGixrbmJTORjg8T15gXlPbFcXJzVlQ8+gGuBcHMjBBFBtqb1ZhXN2IZ6udzrRHCB+ag==", + "dev": true, + "requires": { + "@babel/generator": "7.17.10", + "@babel/parser": "^7.1.3", + "@babel/types": "7.17.10", + "apollo-env": "^0.10.2", + "apollo-language-server": "^1.26.9", + "ast-types": "^0.14.0", + "common-tags": "^1.5.1", + "recast": "^0.21.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "requires": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + } + }, + "@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "apollo-codegen-flow": { + "version": "0.38.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-flow/-/apollo-codegen-flow-0.38.9.tgz", + "integrity": "sha512-w02FRiDCfFH7FxRqKZlnmH6q4URT3hlrGvizaRKirEyFQWH7OSEE4osYhSCU+dcRru+NuWtPVrMh3LTB7NinSQ==", + "dev": true, + "requires": { + "@babel/generator": "7.17.10", + "@babel/types": "7.17.10", + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + }, + "dependencies": { + "@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "requires": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + } + }, + "@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "apollo-codegen-scala": { + "version": "0.39.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-scala/-/apollo-codegen-scala-0.39.9.tgz", + "integrity": "sha512-Dtpg8m3MgJ5RIlkPfGDOclsZro1scR32AQY517uA3QdUHa/R+XxU9CQ2bnPnI7BtzuUsrTiJnBXQSulfxrdDOQ==", + "dev": true, + "requires": { + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + } + }, + "apollo-codegen-swift": { + "version": "0.40.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-swift/-/apollo-codegen-swift-0.40.9.tgz", + "integrity": "sha512-Ghk0ef4//QOUdJ80kheD7Q20o9UDrXQVWXz8lWUM88w1cba5LBLXz+CeeQ+VyUHrnFO9XqkimqyPZSDpDmHUSA==", + "dev": true, + "requires": { + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + } + }, + "apollo-codegen-typescript": { + "version": "0.40.9", + "resolved": "https://registry.npmjs.org/apollo-codegen-typescript/-/apollo-codegen-typescript-0.40.9.tgz", + "integrity": "sha512-koOS3ZbU8UNoZwl87WBxpo+3t0e/iIIkbgYg9zOVKnHCYHi2/CbSE7rq3uAM99QmvcE62wrIoFjpBQADJq78Dw==", + "dev": true, + "requires": { + "@babel/generator": "7.17.10", + "@babel/types": "7.17.10", + "apollo-codegen-core": "^0.40.9", + "change-case": "^4.0.0", + "common-tags": "^1.5.1", + "inflected": "^2.0.3" + }, + "dependencies": { + "@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "requires": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + } + }, + "@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "apollo-datasource": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.3.2.tgz", + "integrity": "sha512-L5TiS8E2Hn/Yz7SSnWIVbZw0ZfEIXZCa5VUiVxD9P53JvSrf4aStvsFDlGWPvpIdCR+aly2CfoB79B9/JjKFqg==", + "dev": true, + "requires": { + "@apollo/utils.keyvaluecache": "^1.0.1", + "apollo-server-env": "^4.2.1" + } + }, + "apollo-env": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.10.2.tgz", + "integrity": "sha512-DlB3ZE0j4eFWAVi14pONNPR6h54FlJwerQeuYAxXm0kPA4sGaScsGDRLsIVMP81JxjjDDmWukw5jp9H+zy39hA==", + "dev": true, + "requires": { + "@types/node-fetch": "^2.5.10", + "core-js": "^3.0.1", + "node-fetch": "^2.6.1", + "sha.js": "^2.4.11" + } + }, + "apollo-graphql": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.9.7.tgz", + "integrity": "sha512-bezL9ItUWUGHTm1bI/XzIgiiZbhXpsC7uxk4UxFPmcVJwJsDc3ayZ99oXxAaK+3Rbg/IoqrHckA6CwmkCsbaSA==", + "dev": true, + "requires": { + "core-js-pure": "^3.10.2", + "lodash.sortby": "^4.7.0", + "sha.js": "^2.4.11" + } + }, + "apollo-language-server": { + "version": "1.26.9", + "resolved": "https://registry.npmjs.org/apollo-language-server/-/apollo-language-server-1.26.9.tgz", + "integrity": "sha512-+moe6KfDPPHUaC5Te4x9O5OqBPTZmkNRfjM4kb3XRb3ve8tUeKdye5lIANU+XCv7aZ6G68PHozrO5/Tj1X8Qcw==", + "dev": true, + "requires": { + "@apollo/federation": "0.27.0", + "@apollographql/apollo-tools": "^0.5.4", + "@apollographql/graphql-language-service-interface": "^2.0.2", + "@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2", + "apollo-datasource": "^3.0.0", + "apollo-env": "^0.10.2", + "apollo-graphql": "^0.9.7", + "apollo-link": "^1.2.3", + "apollo-link-context": "^1.0.9", + "apollo-link-error": "^1.1.1", + "apollo-link-http": "^1.5.5", + "apollo-server-errors": "^2.0.2", + "await-to-js": "^3.0.0", + "core-js": "^3.0.1", + "cosmiconfig": "^7.0.1", + "dotenv": "^16.0.0", + "glob": "^8.0.0", + "graphql": "^15.5.0", + "graphql-tag": "^2.10.1", + "lodash.debounce": "^4.0.8", + "lodash.merge": "^4.6.1", + "minimatch": "^5.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.4", + "vscode-uri": "1.0.6" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "apollo-link": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", + "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", + "dev": true, + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.21" + }, + "dependencies": { + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "zen-observable-ts": { + "version": "0.8.21", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", + "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", + "dev": true, + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } + } + } + }, + "apollo-link-context": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/apollo-link-context/-/apollo-link-context-1.0.20.tgz", + "integrity": "sha512-MLLPYvhzNb8AglNsk2NcL9AvhO/Vc9hn2ZZuegbhRHGet3oGr0YH9s30NS9+ieoM0sGT11p7oZ6oAILM/kiRBA==", + "dev": true, + "requires": { + "apollo-link": "^1.2.14", + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "apollo-link-error": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/apollo-link-error/-/apollo-link-error-1.1.13.tgz", + "integrity": "sha512-jAZOOahJU6bwSqb2ZyskEK1XdgUY9nkmeclCrW7Gddh1uasHVqmoYc4CKdb0/H0Y1J9lvaXKle2Wsw/Zx1AyUg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.14", + "apollo-link-http-common": "^0.2.16", + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "apollo-link-http": { + "version": "1.5.17", + "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz", + "integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.14", + "apollo-link-http-common": "^0.2.16", + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "apollo-link-http-common": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", + "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.14", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3" + }, + "dependencies": { + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "apollo-server-env": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-4.2.1.tgz", + "integrity": "sha512-vm/7c7ld+zFMxibzqZ7SSa5tBENc4B0uye9LTfjJwGoQFY5xsUPH5FpO5j0bMUDZ8YYNbrF9SNtzc5Cngcr90g==", + "dev": true, + "requires": { + "node-fetch": "^2.6.7" + } + }, + "apollo-server-errors": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.5.0.tgz", + "integrity": "sha512-lO5oTjgiC3vlVg2RKr3RiXIIQ5pGXBFxYGGUkKDhTud3jMIhs+gel8L8zsEjKaKxkjHhCQAA/bcEfYiKkGQIvA==", + "dev": true, + "requires": {} + }, + "apollo-utilities": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", + "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", + "dev": true, + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "@wry/equality": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", + "integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + }, + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + } + }, + "array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.reduce": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz", + "integrity": "sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, + "array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", + "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", + "requires": { + "browserslist": "^4.20.3", + "caniuse-lite": "^1.0.30001335", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "dev": true + }, + "babel-plugin-styled-components": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz", + "integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "blurhash": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-1.1.5.tgz", + "integrity": "sha512-a+LO3A2DfxTaTztsmkbLYmUzUeApi0LZuKalwbNmqAHR6HhJGMt1qSV/R3wc+w4DL28holjqO3Bg74aUGavGjg==" + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "requires": { + "fill-range": "^7.1.1" + } + }, + "broccoli-node-api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", + "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", + "dev": true + }, + "broccoli-node-info": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", + "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", + "dev": true + }, + "broccoli-output-wrapper": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", + "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", + "dev": true, + "requires": { + "fs-extra": "^8.1.0", + "heimdalljs-logger": "^0.1.10", + "symlink-or-copy": "^1.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "broccoli-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", + "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", + "dev": true, + "requires": { + "broccoli-node-api": "^1.7.0", + "broccoli-output-wrapper": "^3.2.5", + "fs-merger": "^3.2.1", + "promise-map-series": "^0.3.0", + "quick-temp": "^0.1.8", + "rimraf": "^3.0.2", + "symlink-or-copy": "^1.3.1" + } + }, + "browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "requires": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "c8": { + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz", + "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "dependencies": { + "v8-to-istanbul": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", + "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + } + } + } + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, + "caniuse-lite": { + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==" + }, + "capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "dev": true, + "requires": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + } + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "requires": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true + }, + "cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dev": true, + "requires": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + } + }, + "cheerio-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", + "dev": true, + "requires": { + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, + "clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "dev": true, + "requires": { + "escape-string-regexp": "4.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-progress": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.11.2.tgz", + "integrity": "sha512-lCPoS6ncgX4+rJu5bS3F/iCz17kZ9MPZ6dpuTtI0KXKABkhyXIdYB3Inby1OpaGti3YlI3EeEkM9AuWpelJrVA==", + "dev": true, + "requires": { + "string-width": "^4.2.3" + } + }, + "cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "string-width": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.0.tgz", + "integrity": "sha512-7x54QnN21P+XL/v8SuNKvfgsUre6PXpN7mc77N3HlZv+f1SBRGmjxtOud2Z6FZ8DmdkD/IdjCaf9XXbnqmTZGQ==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "cli-ux": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.9.tgz", + "integrity": "sha512-0Ku29QLf+P6SeBNWM7zyoJ49eKKOjxZBZ4OH2aFeRtC0sNXU3ftdJxQPKJ1SJ+axX34I1NsfTFahpXdnxklZgA==", + "dev": true, + "requires": { + "@oclif/core": "^1.1.1", + "@oclif/linewrap": "^1.0.0", + "@oclif/screen": "^1.0.4 ", + "ansi-escapes": "^4.3.0", + "ansi-styles": "^4.2.0", + "cardinal": "^2.1.1", + "chalk": "^4.1.0", + "clean-stack": "^3.0.0", + "cli-progress": "^3.10.0", + "extract-stack": "^2.0.0", + "fs-extra": "^8.1", + "hyperlinker": "^1.0.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "js-yaml": "^3.13.1", + "lodash": "^4.17.21", + "natural-orderby": "^2.0.1", + "object-treeify": "^1.1.4", + "password-prompt": "^1.1.2", + "semver": "^7.3.2", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "supports-color": "^8.1.0", + "supports-hyperlinks": "^2.1.0", + "tslib": "^2.0.0" + }, + "dependencies": { + "@oclif/screen": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-1.0.4.tgz", + "integrity": "sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", + "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + }, + "common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==" + }, + "constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, + "core-js": { + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.3.tgz", + "integrity": "sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==", + "dev": true + }, + "core-js-pure": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.1.tgz", + "integrity": "sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "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" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + }, + "css-select": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", + "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.1.0", + "domhandler": "^4.3.0", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-to-react-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", + "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" + }, + "css-what": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "dev": true + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true + }, + "csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha1-s085HupNqPPpgjHizNjfnAQfFxs=" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + }, + "data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "dependencies": { + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", + "dev": true + }, + "dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==" + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "requires": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true + }, + "diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-accessibility-api": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz", + "integrity": "sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw==", + "dev": true + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "requires": { + "webidl-conversions": "^7.0.0" + } + }, + "domhandler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", + "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", + "dev": true + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "earcut": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz", + "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug==" + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.733", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz", + "integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ==" + }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "env-ci": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-7.1.0.tgz", + "integrity": "sha512-zyRGZQkjp5lgYYRUJS7hbEAhEtfslzwN5ScSnLXhaF2OEtiVC8LW+5mbaIqlFpIE95iFhukrKaLm0Rdt/w2lNg==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "fromentries": "^1.3.2", + "java-properties": "^1.0.0" + } + }, + "eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + } + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "esbuild": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.48.tgz", + "integrity": "sha512-w6N1Yn5MtqK2U1/WZTX9ZqUVb8IOLZkZ5AdHkT6x3cHDMVsYWC7WPdiLmx19w3i4Rwzy5LqsEMtVihG3e4rFzA==", + "requires": { + "esbuild-android-64": "0.14.48", + "esbuild-android-arm64": "0.14.48", + "esbuild-darwin-64": "0.14.48", + "esbuild-darwin-arm64": "0.14.48", + "esbuild-freebsd-64": "0.14.48", + "esbuild-freebsd-arm64": "0.14.48", + "esbuild-linux-32": "0.14.48", + "esbuild-linux-64": "0.14.48", + "esbuild-linux-arm": "0.14.48", + "esbuild-linux-arm64": "0.14.48", + "esbuild-linux-mips64le": "0.14.48", + "esbuild-linux-ppc64le": "0.14.48", + "esbuild-linux-riscv64": "0.14.48", + "esbuild-linux-s390x": "0.14.48", + "esbuild-netbsd-64": "0.14.48", + "esbuild-openbsd-64": "0.14.48", + "esbuild-sunos-64": "0.14.48", + "esbuild-windows-32": "0.14.48", + "esbuild-windows-64": "0.14.48", + "esbuild-windows-arm64": "0.14.48" + } + }, + "esbuild-android-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.48.tgz", + "integrity": "sha512-3aMjboap/kqwCUpGWIjsk20TtxVoKck8/4Tu19rubh7t5Ra0Yrpg30Mt1QXXlipOazrEceGeWurXKeFJgkPOUg==", + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.48.tgz", + "integrity": "sha512-vptI3K0wGALiDq+EvRuZotZrJqkYkN5282iAfcffjI5lmGG9G1ta/CIVauhY42MBXwEgDJkweiDcDMRLzBZC4g==", + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.48.tgz", + "integrity": "sha512-gGQZa4+hab2Va/Zww94YbshLuWteyKGD3+EsVon8EWTWhnHFRm5N9NbALNbwi/7hQ/hM1Zm4FuHg+k6BLsl5UA==", + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.48.tgz", + "integrity": "sha512-bFjnNEXjhZT+IZ8RvRGNJthLWNHV5JkCtuOFOnjvo5pC0sk2/QVk0Qc06g2PV3J0TcU6kaPC3RN9yy9w2PSLEA==", + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.48.tgz", + "integrity": "sha512-1NOlwRxmOsnPcWOGTB10JKAkYSb2nue0oM1AfHWunW/mv3wERfJmnYlGzL3UAOIUXZqW8GeA2mv+QGwq7DToqA==", + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.48.tgz", + "integrity": "sha512-gXqKdO8wabVcYtluAbikDH2jhXp+Klq5oCD5qbVyUG6tFiGhrC9oczKq3vIrrtwcxDQqK6+HDYK8Zrd4bCA9Gw==", + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.48.tgz", + "integrity": "sha512-ghGyDfS289z/LReZQUuuKq9KlTiTspxL8SITBFQFAFRA/IkIvDpnZnCAKTCjGXAmUqroMQfKJXMxyjJA69c/nQ==", + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.48.tgz", + "integrity": "sha512-vni3p/gppLMVZLghI7oMqbOZdGmLbbKR23XFARKnszCIBpEMEDxOMNIKPmMItQrmH/iJrL1z8Jt2nynY0bE1ug==", + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.48.tgz", + "integrity": "sha512-+VfSV7Akh1XUiDNXgqgY1cUP1i2vjI+BmlyXRfVz5AfV3jbpde8JTs5Q9sYgaoq5cWfuKfoZB/QkGOI+QcL1Tw==", + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.48.tgz", + "integrity": "sha512-3CFsOlpoxlKPRevEHq8aAntgYGYkE1N9yRYAcPyng/p4Wyx0tPR5SBYsxLKcgPB9mR8chHEhtWYz6EZ+H199Zw==", + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.48.tgz", + "integrity": "sha512-cs0uOiRlPp6ymknDnjajCgvDMSsLw5mST2UXh+ZIrXTj2Ifyf2aAP3Iw4DiqgnyYLV2O/v/yWBJx+WfmKEpNLA==", + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.48.tgz", + "integrity": "sha512-+2F0vJMkuI0Wie/wcSPDCqXvSFEELH7Jubxb7mpWrA/4NpT+/byjxDz0gG6R1WJoeDefcrMfpBx4GFNN1JQorQ==", + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.48.tgz", + "integrity": "sha512-BmaK/GfEE+5F2/QDrIXteFGKnVHGxlnK9MjdVKMTfvtmudjY3k2t8NtlY4qemKSizc+QwyombGWTBDc76rxePA==", + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.48.tgz", + "integrity": "sha512-tndw/0B9jiCL+KWKo0TSMaUm5UWBLsfCKVdbfMlb3d5LeV9WbijZ8Ordia8SAYv38VSJWOEt6eDCdOx8LqkC4g==", + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.48.tgz", + "integrity": "sha512-V9hgXfwf/T901Lr1wkOfoevtyNkrxmMcRHyticybBUHookznipMOHoF41Al68QBsqBxnITCEpjjd4yAos7z9Tw==", + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.48.tgz", + "integrity": "sha512-+IHf4JcbnnBl4T52egorXMatil/za0awqzg2Vy6FBgPcBpisDWT2sVz/tNdrK9kAqj+GZG/jZdrOkj7wsrNTKA==", + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.48.tgz", + "integrity": "sha512-77m8bsr5wOpOWbGi9KSqDphcq6dFeJyun8TA+12JW/GAjyfTwVtOnN8DOt6DSPUfEV+ltVMNqtXUeTeMAxl5KA==", + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.48.tgz", + "integrity": "sha512-EPgRuTPP8vK9maxpTGDe5lSoIBHGKO/AuxDncg5O3NkrPeLNdvvK8oywB0zGaAZXxYWfNNSHskvvDgmfVTguhg==", + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.48.tgz", + "integrity": "sha512-YmpXjdT1q0b8ictSdGwH3M8VCoqPpK1/UArze3X199w6u8hUx3V8BhAi1WjbsfDYRBanVVtduAhh2sirImtAvA==", + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.48", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.48.tgz", + "integrity": "sha512-HHaOMCsCXp0rz5BT2crTka6MPWVno121NKApsGs/OIW5QC0ggC69YMGs1aJct9/9FSUF4A1xNE/cLvgB5svR4g==", + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "eslint": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", + "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "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": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "globals": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", + "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "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" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "requires": {} + }, + "eslint-plugin-react": { + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "dev": true, + "requires": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "requires": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "extract-stack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-2.0.0.tgz", + "integrity": "sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "requires": { + "reusify": "^1.0.4" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" + }, + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-merger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", + "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", + "dev": true, + "requires": { + "broccoli-node-api": "^1.7.0", + "broccoli-node-info": "^2.1.0", + "fs-extra": "^8.0.1", + "fs-tree-diff": "^2.0.1", + "walk-sync": "^2.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + } + }, + "fs-tree-diff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", + "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", + "dev": true, + "requires": { + "@types/symlink-or-copy": "^1.2.0", + "heimdalljs-logger": "^0.1.7", + "object-assign": "^4.1.0", + "path-posix": "^1.0.0", + "symlink-or-copy": "^1.1.8" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + } + }, + "git-parse": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/git-parse/-/git-parse-2.1.1.tgz", + "integrity": "sha512-jt4pYVdXFFjZMsFohMBYhPsW4j/XBDas7VctxBbRvKCyvQj5pk/+fe2/9Y1hykLSk3lHeVZyScDaW5cZBXDTyw==", + "dev": true, + "requires": { + "byline": "5.0.0", + "util.promisify": "1.1.1" + } + }, + "git-rev-sync": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-3.0.2.tgz", + "integrity": "sha512-Nd5RiYpyncjLv0j6IONy0lGzAqdRXUaBctuGBbrEA2m6Bn4iDrN/9MeQTXuiquw8AEKL9D2BW0nw5m/lQvxqnQ==", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "graceful-fs": "4.1.15", + "shelljs": "0.8.5" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + } + } + }, + "git-up": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz", + "integrity": "sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "parse-url": "^6.0.0" + } + }, + "git-url-parse": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.6.0.tgz", + "integrity": "sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g==", + "dev": true, + "requires": { + "git-up": "^4.0.0" + } + }, + "gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + } + }, + "glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "requires": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + } + }, + "global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "requires": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "globule": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.3.tgz", + "integrity": "sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + }, + "dependencies": { + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + } + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==" + }, + "graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" + }, + "gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha1-xnYqLx8N4KP8WVohWZ0/rI26Gso=", + "dev": true, + "requires": { + "through2": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "requires": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "dev": true, + "requires": { + "rsvp": "~3.2.1" + }, + "dependencies": { + "rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha1-B8tKXfJa3Z6Cbrxn3Mn9idsn2Eo=", + "dev": true + } + } + }, + "heimdalljs-logger": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", + "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", + "dev": true, + "requires": { + "debug": "^2.2.0", + "heimdalljs": "^0.2.6" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" + }, + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=" + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=" + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, + "html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==" + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "http-call": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", + "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==", + "dev": true, + "requires": { + "content-type": "^1.0.4", + "debug": "^4.1.1", + "is-retry-allowed": "^1.1.0", + "is-stream": "^2.0.0", + "parse-json": "^4.0.0", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "hyperlinker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", + "integrity": "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==", + "dev": true + }, + "i18next": { + "version": "21.8.13", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.13.tgz", + "integrity": "sha512-DpzwrJq7Y8tjUHxx6ByOkUIjrGYdQI5Mfv4XEI7q2RWdknQ7TaO9bKi8hS/LqYD6pBV5YGxJLReyLkOCxIpouA==", + "requires": { + "@babel/runtime": "^7.17.2" + } + }, + "i18next-parser": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-8.13.0.tgz", + "integrity": "sha512-XU7resoeNcpJazh29OncQQUH6HsgCxk06RqBBDAmLHldafxopfCHY1vElyG/o3EY0Sn7XjelAmPTV0SgddJEww==", + "dev": true, + "requires": { + "@babel/runtime": "^7.23.2", + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0-rc.2", + "colors": "1.4.0", + "commander": "~11.1.0", + "eol": "^0.9.1", + "esbuild": "^0.20.1", + "fs-extra": "^11.1.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1", + "js-yaml": "4.1.0", + "lilconfig": "^3.0.0", + "rsvp": "^4.8.2", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "~3.0.0", + "vinyl-fs": "^4.0.0", + "vue-template-compiler": "^2.6.11" + }, + "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 + }, + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true + }, + "esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "i18next": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.1.tgz", + "integrity": "sha512-mXw4A24BiPZKRsbb9ewgSvjYd6fxFCNwJyfK6nYfSTIAX2GkCWcb598m3DFkDZmqADatvuASrKo6qwORz3VwTQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.23.2" + } + }, + "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" + } + }, + "lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true + } + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflected": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflected/-/inflected-2.1.0.tgz", + "integrity": "sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "requires": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "requires": { + "is-typed-array": "^1.1.13" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "dev": true, + "requires": { + "symbol-observable": "^1.1.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + } + } + }, + "is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7" + } + }, + "is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dev": true, + "requires": { + "protocols": "^2.0.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.14" + } + }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true + }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", + "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" + }, + "iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "java-properties": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", + "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", + "dev": true + }, + "jest-diff": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.1.tgz", + "integrity": "sha512-/MUUxeR2fHbqHoMMiffe/Afm+U8U4olFRJ0hiVG2lZatPJcnGxx292ustVu7bULhjV65IYMxRdploAKLbcrsyg==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "pretty-format": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", + "integrity": "sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==", + "requires": { + "@jest/schemas": "^28.0.2", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==" + }, + "jest-matcher-utils": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.1.tgz", + "integrity": "sha512-NPJPRWrbmR2nAJ+1nmnfcKKzSwgfaciCCrYZzVnNoxVoyusYWIjkBMNvu0RHJe7dNj4hH3uZOPZsQA+xAYWqsw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "pretty-format": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", + "integrity": "sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==", + "requires": { + "@jest/schemas": "^28.0.2", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "dependencies": { + "entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true + }, + "form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + } + }, + "parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "requires": { + "entities": "^6.0.0" + } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + }, + "ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "requires": {} + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, + "lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==" + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "lint-staged": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", + "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", + "dev": true, + "requires": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.17", + "commander": "^9.3.0", + "debug": "^4.3.4", + "execa": "^6.1.0", + "lilconfig": "2.0.5", + "listr2": "^4.0.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-inspect": "^1.12.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.1", + "yaml": "^2.1.1" + }, + "dependencies": { + "commander": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", + "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==", + "dev": true + }, + "execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, + "yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "dev": true + } + } + }, + "listr": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", + "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", + "dev": true, + "requires": { + "@samverschueren/stream-to-observable": "^0.3.0", + "is-observable": "^1.1.0", + "is-promise": "^2.1.0", + "is-stream": "^1.1.0", + "listr-silent-renderer": "^1.1.1", + "listr-update-renderer": "^0.5.0", + "listr-verbose-renderer": "^0.5.0", + "p-map": "^2.0.0", + "rxjs": "^6.3.3" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + } + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", + "dev": true + }, + "listr-update-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", + "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", + "log-symbols": "^1.0.2", + "log-update": "^2.3.0", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "requires": { + "slice-ansi": "0.0.4", + "string-width": "^1.0.1" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "listr-verbose-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", + "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "cli-cursor": "^2.1.0", + "date-fns": "^1.27.2", + "figures": "^2.0.0" + }, + "dependencies": { + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + } + } + }, + "listr2": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", + "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", + "dev": true, + "requires": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.5", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "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" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rxjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", + "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + } + } + }, + "load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + } + } + }, + "local-pkg": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.1.tgz", + "integrity": "sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.identity": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz", + "integrity": "sha1-rXvGpOZH15yXLhuA/u968VYmeHY=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=" + }, + "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.xorby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.xorby/-/lodash.xorby-4.7.0.tgz", + "integrity": "sha512-gYiD6nvuQy0AEkMoUju+t4f4Rn18fjsLB/7x7YZFqtFT9kmegRLrj/uGEQVyVDy7otTmSrIMXNOk2wwuLcfHCQ==", + "dev": true + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "log-update": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "cli-cursor": "^2.0.0", + "wrap-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + } + } + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==" + }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, + "mapbox-gl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.9.1.tgz", + "integrity": "sha512-PWEyI0FUeWaCbDnKmzHBi6CIB264SG6O894PJL1q4Cz/ZiHNyAROonCD48bxVWSQnXXAEDPyhJS5MiJ2K5DTjw==", + "requires": { + "@mapbox/geojson-rewind": "^0.5.1", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^2.0.1", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.5", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.3", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.2", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.4", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" + } + }, + "matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "requires": { + "escape-string-regexp": "^4.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + } + } + }, + "matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + } + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "mktemp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", + "integrity": "sha1-bQUVYRyKjITkhKogABKbmOmB/ws=", + "dev": true + }, + "modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==" + }, + "moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true + }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E=" + }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "natural-orderby": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz", + "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "requires": { + "lodash": "^4.17.21" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==" + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, + "object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true + }, + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz", + "integrity": "sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==", + "dev": true, + "requires": { + "array.prototype.reduce": "^1.0.4", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.1" + } + }, + "object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optimism": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", + "integrity": "sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==", + "requires": { + "@wry/context": "^0.6.0", + "@wry/trie": "^0.3.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-path": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.4.tgz", + "integrity": "sha512-Z2lWUis7jlmXC1jeOG9giRO2+FsuyNipeQ43HAjqAZjwSe3SEf+q/84FGPHoso3kyntbxa4c4i77t3m6fGf8cw==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0", + "qs": "^6.9.4", + "query-string": "^6.13.8" + }, + "dependencies": { + "protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "dev": true + } + } + }, + "parse-url": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-6.0.5.tgz", + "integrity": "sha512-e35AeLTSIlkw/5GFq70IN7po8fmDUjpDPY1rIK+VubRfsUvBonjQ+PBZG+vWMACnQSmNlvl524IucoDmcioMxA==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "normalize-url": "^6.1.0", + "parse-path": "^4.0.0", + "protocols": "^1.4.0" + }, + "dependencies": { + "protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "dev": true + } + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "password-prompt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.2.tgz", + "integrity": "sha512-bpuBhROdrhuN3E7G/koAju0WjVw9/uQOG5Co5mokNj0MiOSBVZS1JTwM4zl55hu0WFmIEFvO9cU9sJQiBIYeIA==", + "dev": true, + "requires": { + "ansi-escapes": "^3.1.0", + "cross-spawn": "^6.0.5" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "requires": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + } + }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true + }, + "postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + } + }, + "postcss-functions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", + "integrity": "sha512-N5yWXWKA+uhpLQ9ZhBRl2bIAdM6oVJYpDojuI1nF2SzXBimJcdjFwiAouBVbO5VuOF3qA6BSFWFc3wXbbj72XQ==", + "requires": { + "glob": "^7.1.2", + "object-assign": "^4.1.1", + "postcss": "^6.0.9", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "postcss-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-2.0.3.tgz", + "integrity": "sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==", + "requires": { + "camelcase-css": "^2.0.1", + "postcss": "^7.0.18" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + } + } + }, + "postcss-nested": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-4.2.3.tgz", + "integrity": "sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw==", + "requires": { + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + } + } + }, + "postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==" + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-map-series": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", + "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, + "protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true + }, + "psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "purgecss": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", + "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", + "requires": { + "commander": "^8.0.0", + "glob": "^7.1.7", + "postcss": "^8.3.5", + "postcss-selector-parser": "^6.0.6" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "query-string": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", + "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, + "quick-temp": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", + "integrity": "sha1-urAqJCq4+w3XWKPJd2sy+aXZRAg=", + "dev": true, + "requires": { + "mktemp": "~0.4.0", + "rimraf": "^2.5.4", + "underscore.string": "~3.3.4" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-blurhash": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.1.3.tgz", + "integrity": "sha512-Q9lqbXg92NU6/2DoIl/cBM8YWL+Z4X66OiG4aT9ozOgjBwx104LHFCH5stf6aF+s0Q9Wf310Ul+dG+VXJltmPg==", + "requires": {} + }, + "react-datepicker": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.0.0.tgz", + "integrity": "sha512-LGzKgBk5NUEcXUeSEJY2ICCCmPusm9UGRkNKRXCPgpUzoMx2CCsa0mOHTyv+itQeW7tR/jgGEpq5q/afZjGEFg==", + "requires": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "dependencies": { + "date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + } + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, + "react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + } + }, + "react-hook-form": { + "version": "7.33.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.33.1.tgz", + "integrity": "sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg==", + "requires": {} + }, + "react-i18next": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.0.tgz", + "integrity": "sha512-coJujU20xJ5Wa5rHjTyB5LFKZb1yfXo2A+40RRSyAF0FlZRHyy+3C1Mr92x1JPfS7W7v2TWNn8mRhpDFGJwXVg==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "requires": { + "history": "^5.2.0" + } + }, + "react-router-dom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", + "requires": { + "history": "^5.2.0", + "react-router": "6.3.0" + } + }, + "react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "requires": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + } + }, + "react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "requires": {} + }, + "react-swipeable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.0.tgz", + "integrity": "sha512-NI7KGfQ6gwNFN0Hor3vytYW3iRfMMaivGEuxcADOOfBCx/kqwXE8IfHFxEcxSUkxCYf38COLKYd9EMYZghqaUA==", + "requires": {} + }, + "react-test-renderer": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", + "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", + "requires": { + "react-is": "^18.2.0", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "recast": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.1.tgz", + "integrity": "sha512-PF61BHLaOGF5oIKTpSrDM6Qfy2d7DIx5qblgqG+wjqHuFH97OgAqBYFIJwEuHTrM6pQGT17IJ8D0C/jVu/0tig==", + "dev": true, + "requires": { + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "dependencies": { + "ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + } + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "dev": true, + "requires": { + "esprima": "~4.0.0" + } + }, + "reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + } + } + }, + "reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "requires": { + "value-or-function": "^4.0.0" + } + }, + "resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "requires": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + } + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=" + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "requires": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, + "rollup": { + "version": "2.66.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.66.1.tgz", + "integrity": "sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==", + "requires": { + "fsevents": "~2.3.2" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true + }, + "sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "requires": { + "type-fest": "^0.13.1" + }, + "dependencies": { + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + } + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + } + } + }, + "sirv": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.2.tgz", + "integrity": "sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", + "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "dev": true + } + } + }, + "snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dev": true, + "requires": { + "is-plain-obj": "^4.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" + }, + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "requires": { + "streamx": "^2.13.2" + } + }, + "streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + } + } + }, + "string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "strip-ansi": { + "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.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "styled-components": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz", + "integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + } + }, + "subscriptions-transport-ws": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz", + "integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==", + "requires": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0 || ^6.0.0 || ^7.0.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + } + } + }, + "supercluster": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz", + "integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==", + "requires": { + "kdbush": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==" + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "dev": true + }, + "tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==" + }, + "table": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", + "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ajv": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz", + "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", + "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" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "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" + } + } + } + }, + "tailwind-override": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/tailwind-override/-/tailwind-override-0.6.1.tgz", + "integrity": "sha512-FOxLFcXTe9eqC85SNCREyWw/CG8hyNSBJNrp+rjMDV7m63UoJzpkNJtXNrA0y3tE9m0d3ccNj8HBVQG/hfmNxA==" + }, + "tailwindcss": { + "version": "npm:@tailwindcss/postcss7-compat@2.2.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.17.tgz", + "integrity": "sha512-3h2svqQAqYHxRZ1KjsJjZOVTQ04m29LjfrLjXyZZEJuvUuJN+BCIF9GI8vhE1s0plS0mogd6E6YLg6mu4Wv/Vw==", + "requires": { + "arg": "^5.0.1", + "autoprefixer": "^9", + "bytes": "^3.0.0", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "color": "^4.0.1", + "cosmiconfig": "^7.0.1", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.7", + "fs-extra": "^10.0.0", + "glob-parent": "^6.0.1", + "html-tags": "^3.1.0", + "is-color-stop": "^1.1.0", + "is-glob": "^4.0.1", + "lodash": "^4.17.21", + "lodash.topath": "^4.5.2", + "modern-normalize": "^1.1.0", + "node-emoji": "^1.11.0", + "normalize-path": "^3.0.0", + "object-hash": "^2.2.0", + "postcss": "^7", + "postcss-functions": "^3", + "postcss-js": "^2", + "postcss-load-config": "^3.1.0", + "postcss-nested": "^4", + "postcss-selector-parser": "^6.0.6", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", + "quick-lru": "^5.1.1", + "reduce-css-calc": "^2.1.8", + "resolve": "^1.20.0", + "tmp": "^0.2.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "autoprefixer": { + "version": "9.8.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", + "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "picocolors": "^0.2.1", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.0.tgz", + "integrity": "sha512-hHTcrbvEnGjC7WBMk6ibQWFVDgEFTVmjrz2Q5HlU6ltwxv0JJN2Z8I7uRbWeQLF04dikxs8zgyZkazRJvSMtyQ==", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "postcss-load-config": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.1.tgz", + "integrity": "sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg==", + "requires": { + "lilconfig": "^2.0.4", + "yaml": "^1.10.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "tinypool": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.2.2.tgz", + "integrity": "sha512-tp4n5OARNL3v8ntdJUyo5NsDfwvUtu8isB43USjrsQxQrADDKY6UGBkmFaw/2vNmEt8S/uSm2U5FhkiK1eAFGw==", + "dev": true + }, + "tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, + "tinyspy": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-0.3.3.tgz", + "integrity": "sha512-gRiUR8fuhUf0W9lzojPf1N1euJYA30ISebSfgca8z76FOvXtVXqd5ojEIaKLWbDQhAaC3ibxZIjqbyi4ybjcTw==", + "dev": true + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, + "totalist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", + "integrity": "sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==", + "dev": true + }, + "touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true + }, + "tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + } + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "requires": { + "tslib": "^2.1.0" + } + }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true + } + } + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "tty": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tty/-/tty-1.0.1.tgz", + "integrity": "sha1-5ECayYsN0cULWf846G6sPwdk7kU=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } + }, + "typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "devOptional": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "requires": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==" + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.1.tgz", + "integrity": "sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "for-each": "^0.3.3", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.1" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, + "value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + }, + "vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "requires": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + } + }, + "vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + } + }, + "vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "requires": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + } + } + }, + "vite": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.18.tgz", + "integrity": "sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==", + "requires": { + "esbuild": "^0.14.27", + "fsevents": "~2.3.2", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" + } + }, + "vite-plugin-svgr": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.2.0.tgz", + "integrity": "sha512-CRJgz92vf3SQbt3jwF6/BhyQIhbY0pwbXAPqUh1/EMG6a4eHsHCECcDNrKZMhlRBj3LyXQSfHXJLtM2ISP5rfg==", + "requires": { + "@rollup/pluginutils": "^4.2.1", + "@svgr/core": "^6.2.1" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "@svgr/babel-plugin-add-jsx-attribute": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.0.0.tgz", + "integrity": "sha512-MdPdhdWLtQsjd29Wa4pABdhWbaRMACdM1h31BY+c6FghTZqNGT7pEYdBoaGeKtdTOBC/XNFQaKVj+r/Ei2ryWA==", + "requires": {} + }, + "@svgr/babel-plugin-remove-jsx-attribute": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.0.0.tgz", + "integrity": "sha512-aVdtfx9jlaaxc3unA6l+M9YRnKIZjOhQPthLKqmTXC8UVkBLDRGwPKo+r8n3VZN8B34+yVajzPTZ+ptTSuZZCw==", + "requires": {} + }, + "@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.0.0.tgz", + "integrity": "sha512-Ccj42ApsePD451AZJJf1QzTD1B/BOU392URJTeXFxSK709i0KUsGtbwyiqsKu7vsYxpTM0IA5clAKDyf9RCZyA==", + "requires": {} + }, + "@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.0.0.tgz", + "integrity": "sha512-88V26WGyt1Sfd1emBYmBJRWMmgarrExpKNVmI9vVozha4kqs6FzQJ/Kp5+EYli1apgX44518/0+t9+NU36lThQ==", + "requires": {} + }, + "@svgr/babel-plugin-svg-dynamic-title": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.0.0.tgz", + "integrity": "sha512-F7YXNLfGze+xv0KMQxrl2vkNbI9kzT9oDK55/kUuymh1ACyXkMV+VZWX1zEhSTfEKh7VkHVZGmVtHg8eTZ6PRg==", + "requires": {} + }, + "@svgr/babel-plugin-svg-em-dimensions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.0.0.tgz", + "integrity": "sha512-+rghFXxdIqJNLQK08kwPBD3Z22/0b2tEZ9lKiL/yTfuyj1wW8HUXu4bo/XkogATIYuXSghVQOOCwURXzHGKyZA==", + "requires": {} + }, + "@svgr/babel-plugin-transform-react-native-svg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.0.0.tgz", + "integrity": "sha512-VaphyHZ+xIKv5v0K0HCzyfAaLhPGJXSk2HkpYfXIOKb7DjLBv0soHDxNv6X0vr2titsxE7klb++u7iOf7TSrFQ==", + "requires": {} + }, + "@svgr/babel-plugin-transform-svg-component": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.2.0.tgz", + "integrity": "sha512-bhYIpsORb++wpsp91fymbFkf09Z/YEKR0DnFjxvN+8JHeCUD2unnh18jIMKnDJTWtvpTaGYPXELVe4OOzFI0xg==", + "requires": {} + }, + "@svgr/babel-preset": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.2.0.tgz", + "integrity": "sha512-4WQNY0J71JIaL03DRn0vLiz87JXx0b9dYm2aA8XHlQJQoixMl4r/soYHm8dsaJZ3jWtkCiOYy48dp9izvXhDkQ==", + "requires": { + "@svgr/babel-plugin-add-jsx-attribute": "^6.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^6.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^6.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "^6.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "^6.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "^6.0.0", + "@svgr/babel-plugin-transform-svg-component": "^6.2.0" + } + }, + "@svgr/core": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.2.1.tgz", + "integrity": "sha512-NWufjGI2WUyrg46mKuySfviEJ6IxHUOm/8a3Ph38VCWSp+83HBraCQrpEM3F3dB6LBs5x8OElS8h3C0oOJaJAA==", + "requires": { + "@svgr/plugin-jsx": "^6.2.1", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.1" + } + }, + "@svgr/hast-util-to-babel-ast": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.2.1.tgz", + "integrity": "sha512-pt7MMkQFDlWJVy9ULJ1h+hZBDGFfSCwlBNW1HkLnVi7jUhyEXUaGYWi1x6bM2IXuAR9l265khBT4Av4lPmaNLQ==", + "requires": { + "@babel/types": "^7.15.6", + "entities": "^3.0.1" + } + }, + "@svgr/plugin-jsx": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.2.1.tgz", + "integrity": "sha512-u+MpjTsLaKo6r3pHeeSVsh9hmGRag2L7VzApWIaS8imNguqoUwDq/u6U/NDmYs/KAsrmtBjOEaAAPbwNGXXp1g==", + "requires": { + "@babel/core": "^7.15.5", + "@svgr/babel-preset": "^6.2.0", + "@svgr/hast-util-to-babel-ast": "^6.2.1", + "svg-parser": "^2.0.2" + } + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==" + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + } + } + }, + "vitest": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.17.1.tgz", + "integrity": "sha512-d6NsFC6FPmZ5XdiSYfW5rwJ/b8060wqe2steNNlVbhO69HWma6CucIm5g7PXlCSkmKvrdEbUsZHPAarlH83VGw==", + "dev": true, + "requires": { + "@types/chai": "^4.3.1", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.1", + "tinypool": "^0.2.1", + "tinyspy": "^0.3.3", + "vite": "^2.9.12 || ^3.0.0-0" + } + }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" + }, + "vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "dev": true + }, + "vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dev": true, + "requires": { + "vscode-languageserver-protocol": "3.16.0" + } + }, + "vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dev": true, + "requires": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz", + "integrity": "sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg==", + "dev": true + }, + "vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "dev": true + }, + "vscode-uri": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.6.tgz", + "integrity": "sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==", + "dev": true + }, + "vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "requires": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + } + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-builtin-type": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "requires": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, + "which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "requires": { + "string-width": "^4.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "requires": {} + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, + "yarn": { + "version": "1.22.19", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", + "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "requires": { + "zen-observable": "0.8.15" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..0dd218e --- /dev/null +++ b/ui/package.json @@ -0,0 +1,119 @@ +{ + "name": "photoview-ui", + "version": "0.0.1", + "repository": { + "type": "git", + "url": "https://github.com/photoview/photoview.git", + "directory": "ui" + }, + "license": "GPL-3.0", + "description": "UI app for Photoview", + "scripts": { + "start": "vite --host=0.0.0.0", + "mon": "nodemon", + "build": "vite build", + "build:dev": "vite build --mode development --sourcemap --minify=false", + "lint": "eslint ./src --max-warnings 0 --config .eslintrc.js", + "lint:ci": "eslint ./src --config .eslintrc.js --no-color --fix-dry-run > eslint-report.txt", + "lint:staged": "lint-staged", + "format:check": "prettier --check .", + "test": "vitest", + "test:ci": "CI=true vitest --reporter=junit --reporter=verbose --run --coverage", + "genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts --passthroughCustomScalars && prettier --write */**/__generated__/*.ts", + "extractTranslations": "i18next -c i18next-parser.config.js" + }, + "dependencies": { + "@apollo/client": "^3.6.9", + "@babel/preset-typescript": "^7.18.6", + "@headlessui/react": "^1.6.6", + "@types/geojson": "^7946.0.8", + "@types/jest": "^28.1.4", + "@types/js-cookie": "^3.0.6", + "@types/luxon": "^3.7.1", + "@types/mapbox-gl": "^2.7.3", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "@types/react-helmet": "^6.1.5", + "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.25", + "@types/url-join": "^4.0.1", + "@vitejs/plugin-react": "^1.3.2", + "autoprefixer": "^10.4.7", + "blurhash": "^1.1.5", + "classnames": "^2.3.1", + "connect-history-api-fallback": "^2.0.0", + "copy-to-clipboard": "^3.3.1", + "dayjs": "^1.11.19", + "i18next": "^21.8.13", + "js-cookie": "^3.0.5", + "luxon": "^3.7.2", + "mapbox-gl": "^2.9.1", + "postcss": "^8.4.39", + "prettier": "^2.7.1", + "react": "^18.2.0", + "react-blurhash": "^0.1.3", + "react-datepicker": "^9.0.0", + "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", + "react-hook-form": "^7.33.1", + "react-i18next": "^11.18.0", + "react-router-dom": "^6.3.0", + "react-swipeable": "^7.0.0", + "react-test-renderer": "^18.2.0", + "styled-components": "^5.3.5", + "subscriptions-transport-ws": "^0.11.0", + "tailwind-override": "^0.6.1", + "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17", + "url-join": "^5.0.0", + "vite": "^2.9.13", + "vite-plugin-svgr": "^2.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^14.2.1", + "@typescript-eslint/eslint-plugin": "^5.30.6", + "@typescript-eslint/parser": "^5.30.6", + "@vitest/ui": "^0.17.1", + "apollo": "2.34.0", + "apollo-language-server": "1.26.9", + "c8": "^7.11.3", + "eslint": "^8.19.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-react": "^7.37.1", + "eslint-plugin-react-hooks": "^4.6.2", + "i18next-parser": "^8.13.0", + "jsdom": "^20.0.0", + "lint-staged": "^13.0.3", + "nodemon": "^3.1.4", + "ts-node": "^10.9.2", + "typescript": "^5.5.3", + "vitest": "^0.17.1" + }, + "overrides": { + "graphql": "^15.5.0" + }, + "prettier": { + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "arrowParens": "avoid" + }, + "lint-staged": { + "*.{ts,tsx,js,json,css,md,graphql}": "prettier --write", + "*.{js,ts,tsx}": "eslint --cache --fix --max-warnings 0" + }, + "browserslist": { + "production": [ + "defaults", + "not ie 11", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/ui/postcss.config.js b/ui/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/ui/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png new file mode 100644 index 0000000..43fa882 Binary files /dev/null and b/ui/public/apple-touch-icon.png differ diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000..2dec52e Binary files /dev/null and b/ui/public/favicon.ico differ diff --git a/ui/public/fonts/iAWriterQuattroSRegular.ttf b/ui/public/fonts/iAWriterQuattroSRegular.ttf new file mode 100644 index 0000000..f8eb282 Binary files /dev/null and b/ui/public/fonts/iAWriterQuattroSRegular.ttf differ diff --git a/ui/public/fonts/noto-sans-sc-400.ttf b/ui/public/fonts/noto-sans-sc-400.ttf new file mode 100644 index 0000000..9271486 Binary files /dev/null and b/ui/public/fonts/noto-sans-sc-400.ttf differ diff --git a/ui/public/fonts/noto-sans-sc-500.ttf b/ui/public/fonts/noto-sans-sc-500.ttf new file mode 100644 index 0000000..d6d2298 Binary files /dev/null and b/ui/public/fonts/noto-sans-sc-500.ttf differ diff --git a/ui/public/fonts/noto-sans-sc-700.ttf b/ui/public/fonts/noto-sans-sc-700.ttf new file mode 100644 index 0000000..1decf2b Binary files /dev/null and b/ui/public/fonts/noto-sans-sc-700.ttf differ diff --git a/ui/public/logo192.png b/ui/public/logo192.png new file mode 100644 index 0000000..0550eb9 Binary files /dev/null and b/ui/public/logo192.png differ diff --git a/ui/public/logo512.png b/ui/public/logo512.png new file mode 100644 index 0000000..ae93f13 Binary files /dev/null and b/ui/public/logo512.png differ diff --git a/ui/public/manifest.json b/ui/public/manifest.json new file mode 100644 index 0000000..36ff4e9 --- /dev/null +++ b/ui/public/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "Photoview", + "short_name": "Photoview", + "description": "Photo gallery for self-hosted personal servers", + "start_url": "/", + "display": "standalone", + "theme_color": "black", + "background_color": "white", + "icons": [ + { + "src": "/assets/logo192.png", + "sizes": "192x192" + }, + { + "src": "/assets/logo512.png", + "sizes": "512x512" + }, + { + "src": "/assets/photoview-logo.svg", + "sizes": "150x150" + } + ] +} diff --git a/ui/public/photoview-logo.svg b/ui/public/photoview-logo.svg new file mode 100644 index 0000000..b810257 --- /dev/null +++ b/ui/public/photoview-logo.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/@types/index.d.ts b/ui/src/@types/index.d.ts new file mode 100644 index 0000000..8cea417 --- /dev/null +++ b/ui/src/@types/index.d.ts @@ -0,0 +1,20 @@ +declare module '*.svg' { + const ReactComponent: React.FC> + // const content: string + + export { ReactComponent } + // export default content +} + +interface ImportMetaEnv { + readonly REACT_APP_BUILD_VERSION: string | undefined + readonly REACT_APP_BUILD_DATE: string | undefined + readonly REACT_APP_BUILD_COMMIT_SHA: string | undefined +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +type Time = string +type Any = object diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..170a68b --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,38 @@ +import React, { useEffect } from 'react' +import { Helmet } from 'react-helmet' +import Routes from './components/routes/Routes' +import Messages from './components/messages/Messages' +import { useTranslation } from 'react-i18next' +import { loadTranslations } from './localization' +import { useLocation } from 'react-router' + +const App = () => { + const { t } = useTranslation() + const { pathname } = useLocation() + + loadTranslations() + + useEffect(() => { + window.scrollTo(0, 0) + if (document.activeElement != document.body) + (document.activeElement as HTMLInputElement).blur() + }, [pathname]) + + return ( + <> + + + + + + + ) +} + +export default App diff --git a/ui/src/Pages/AlbumPage/AlbumPage.test.tsx b/ui/src/Pages/AlbumPage/AlbumPage.test.tsx new file mode 100644 index 0000000..04cfb64 --- /dev/null +++ b/ui/src/Pages/AlbumPage/AlbumPage.test.tsx @@ -0,0 +1,24 @@ +import { MockedProvider } from '@apollo/client/testing' +import { render, screen } from '@testing-library/react' +import React from 'react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' +import AlbumPage from './AlbumPage' + +vi.mock('../../hooks/useScrollPagination') + +test('AlbumPage renders', () => { + render( + + + + } /> + + + + ) + + expect(screen.getByText('Sort')).toBeInTheDocument() + expect(screen.getByLabelText('Sort direction')).toBeInTheDocument() + + screen.debug() +}) diff --git a/ui/src/Pages/AlbumPage/AlbumPage.tsx b/ui/src/Pages/AlbumPage/AlbumPage.tsx new file mode 100644 index 0000000..ec09578 --- /dev/null +++ b/ui/src/Pages/AlbumPage/AlbumPage.tsx @@ -0,0 +1,120 @@ +import React, { useCallback } from 'react' +import { useQuery, gql } from '@apollo/client' +import AlbumGallery, { + ALBUM_GALLERY_FRAGMENT, +} from '../../components/albumGallery/AlbumGallery' +import Layout from '../../components/layout/Layout' +import useURLParameters from '../../hooks/useURLParameters' +import useScrollPagination from '../../hooks/useScrollPagination' +import PaginateLoader from '../../components/PaginateLoader' +import { useTranslation } from 'react-i18next' +import { albumQuery, albumQueryVariables } from './__generated__/albumQuery' +import useOrderingParams from '../../hooks/useOrderingParams' +import { useParams } from 'react-router-dom' +import { isNil } from '../../helpers/utils' + +const ALBUM_QUERY = gql` + ${ALBUM_GALLERY_FRAGMENT} + + query albumQuery( + $id: ID! + $onlyFavorites: Boolean + $mediaOrderBy: String + $orderDirection: OrderDirection + $limit: Int + $offset: Int + ) { + album(id: $id) { + ...AlbumGalleryFields + } + } +` + +let refetchNeededAll = false +let refetchNeededFavorites = false + +function AlbumPage() { + const { id: albumId } = useParams() + if (isNil(albumId)) + throw new Error('Expected parameter `id` to be defined for AlbumPage') + + const { t } = useTranslation() + + const urlParams = useURLParameters() + const orderParams = useOrderingParams(urlParams) + + const onlyFavorites = urlParams.getParam('favorites') == '1' ? true : false + const setOnlyFavorites = (favorites: boolean) => + urlParams.setParam('favorites', favorites ? '1' : '0') + + const { loading, error, data, refetch, fetchMore } = useQuery< + albumQuery, + albumQueryVariables + >(ALBUM_QUERY, { + variables: { + id: albumId, + onlyFavorites, + mediaOrderBy: orderParams.orderBy, + orderDirection: orderParams.orderDirection, + offset: 0, + limit: 200, + }, + }) + + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.album.media, + }) + + const toggleFavorites = useCallback( + (onlyFavorites: boolean) => { + if ( + (refetchNeededAll && !onlyFavorites) || + (refetchNeededFavorites && onlyFavorites) + ) { + refetch({ id: albumId, onlyFavorites: onlyFavorites }).then(() => { + if (onlyFavorites) { + refetchNeededFavorites = false + } else { + refetchNeededAll = false + } + setOnlyFavorites(onlyFavorites) + }) + } else { + setOnlyFavorites(onlyFavorites) + } + }, + [setOnlyFavorites, refetch] + ) + + if (error) return
Error
+ + return ( + + (refetchNeededAll = refetchNeededFavorites = true)} + showFilter + setOrdering={orderParams.setOrdering} + ordering={orderParams} + /> + + + ) +} + +export default AlbumPage diff --git a/ui/src/Pages/AlbumPage/__generated__/albumQuery.ts b/ui/src/Pages/AlbumPage/__generated__/albumQuery.ts new file mode 100644 index 0000000..3aaad18 --- /dev/null +++ b/ui/src/Pages/AlbumPage/__generated__/albumQuery.ts @@ -0,0 +1,123 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { OrderDirection, MediaType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: albumQuery +// ==================================================== + +export interface albumQuery_album_subAlbums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface albumQuery_album_subAlbums_thumbnail { + __typename: "Media"; + id: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: albumQuery_album_subAlbums_thumbnail_thumbnail | null; +} + +export interface albumQuery_album_subAlbums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: albumQuery_album_subAlbums_thumbnail | null; +} + +export interface albumQuery_album_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface albumQuery_album_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface albumQuery_album_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface albumQuery_album_media { + __typename: "Media"; + id: string; + type: MediaType; + /** + * A short string that can be used to generate a blured version of the media, to show while the original is loading + */ + blurhash: string | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: albumQuery_album_media_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: albumQuery_album_media_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: albumQuery_album_media_videoWeb | null; + favorite: boolean; +} + +export interface albumQuery_album { + __typename: "Album"; + id: string; + title: string; + /** + * The albums contained in this album + */ + subAlbums: albumQuery_album_subAlbums[]; + /** + * The media inside this album + */ + media: albumQuery_album_media[]; +} + +export interface albumQuery { + /** + * Get album by id, user must own the album or be admin + * If valid tokenCredentials are provided, the album may be retrived without further authentication + */ + album: albumQuery_album; +} + +export interface albumQueryVariables { + id: string; + onlyFavorites?: boolean | null; + mediaOrderBy?: string | null; + orderDirection?: OrderDirection | null; + limit?: number | null; + offset?: number | null; +} diff --git a/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx b/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx new file mode 100644 index 0000000..abc19c6 --- /dev/null +++ b/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import AlbumBoxes from '../../components/albumGallery/AlbumBoxes' +import Layout from '../../components/layout/Layout' +import { useQuery, gql } from '@apollo/client' +import { getMyAlbums, getMyAlbumsVariables } from './__generated__/getMyAlbums' +import useURLParameters from '../../hooks/useURLParameters' +import useOrderingParams from '../../hooks/useOrderingParams' +import AlbumFilter from '../../components/album/AlbumFilter' + +const getAlbumsQuery = gql` + query getMyAlbums($orderBy: String, $orderDirection: OrderDirection) { + myAlbums( + order: { order_by: $orderBy, order_direction: $orderDirection } + onlyRoot: true + showEmpty: true + ) { + id + title + thumbnail { + id + thumbnail { + url + } + } + } + } +` + +const AlbumsPage = () => { + const { t } = useTranslation() + + const urlParams = useURLParameters() + const orderParams = useOrderingParams(urlParams, 'updated_at') + + const { error, data } = useQuery( + getAlbumsQuery, + { + variables: { + orderBy: orderParams.orderBy, + orderDirection: orderParams.orderDirection, + }, + } + ) + + const sortingOptions = React.useMemo( + () => [ + { + value: 'updated_at' as const, + label: t('album_filter.sorting_options.date_imported', 'Date imported'), + }, + { + value: 'title' as const, + label: t('album_filter.sorting_options.title', 'Title'), + }, + ], + [t] + ) + + return ( + + + + + ) +} + +export default AlbumsPage diff --git a/ui/src/Pages/AllAlbumsPage/__generated__/getMyAlbums.ts b/ui/src/Pages/AllAlbumsPage/__generated__/getMyAlbums.ts new file mode 100644 index 0000000..ab214c2 --- /dev/null +++ b/ui/src/Pages/AllAlbumsPage/__generated__/getMyAlbums.ts @@ -0,0 +1,49 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { OrderDirection } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: getMyAlbums +// ==================================================== + +export interface getMyAlbums_myAlbums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface getMyAlbums_myAlbums_thumbnail { + __typename: "Media"; + id: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null; +} + +export interface getMyAlbums_myAlbums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: getMyAlbums_myAlbums_thumbnail | null; +} + +export interface getMyAlbums { + /** + * List of albums owned by the logged in user. + */ + myAlbums: getMyAlbums_myAlbums[]; +} + +export interface getMyAlbumsVariables { + orderBy?: string | null; + orderDirection?: OrderDirection | null; +} diff --git a/ui/src/Pages/LoginPage/InitialSetupPage.test.tsx b/ui/src/Pages/LoginPage/InitialSetupPage.test.tsx new file mode 100644 index 0000000..6426ad6 --- /dev/null +++ b/ui/src/Pages/LoginPage/InitialSetupPage.test.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { MockedProvider } from '@apollo/client/testing' +import { render, screen, waitFor } from '@testing-library/react' +import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom' +import { createMemoryHistory } from 'history' +import * as authentication from '../../helpers/authentication' +import InitialSetupPage from './InitialSetupPage' +import { mockInitialSetupGraphql } from './loginTestHelpers' + +vi.mock('../../helpers/authentication.ts') + +const authToken = vi.mocked(authentication.authToken) + +describe('Initial setup page', () => { + test('Render initial setup form', () => { + authToken.mockImplementation(() => null) + + const history = createMemoryHistory({ + initialEntries: ['/initialSetup'], + }) + + render( + + + + + + ) + + expect(screen.getByLabelText('Username')).toBeInTheDocument() + expect(screen.getByLabelText('Password')).toBeInTheDocument() + expect(screen.getByLabelText('Photo path')).toBeInTheDocument() + expect(screen.getByDisplayValue('Setup Photoview')).toBeInTheDocument() + }) + + test('Redirect if auth token is present', async () => { + authToken.mockImplementation(() => 'some-token') + + const history = createMemoryHistory({ + initialEntries: ['/initialSetup'], + }) + + render( + + + + + + ) + + await waitFor(() => { + expect(history.location.pathname).toBe('/') + }) + }) + + test('Redirect if not initial setup', async () => { + authToken.mockImplementation(() => null) + + const history = createMemoryHistory({ + initialEntries: ['/initialSetup'], + }) + + render( + + + + + + ) + + await waitFor(() => { + expect(history.location.pathname).toBe('/') + }) + }) +}) diff --git a/ui/src/Pages/LoginPage/InitialSetupPage.tsx b/ui/src/Pages/LoginPage/InitialSetupPage.tsx new file mode 100644 index 0000000..9c51446 --- /dev/null +++ b/ui/src/Pages/LoginPage/InitialSetupPage.tsx @@ -0,0 +1,155 @@ +import React, { useEffect } from 'react' +import { gql, useQuery, useMutation } from '@apollo/client' +import { useNavigate } from 'react-router-dom' +import { Container } from './loginUtilities' + +import { INITIAL_SETUP_QUERY, login } from './loginUtilities' +import { authToken } from '../../helpers/authentication' +import { useTranslation } from 'react-i18next' +import { CheckInitialSetup } from './__generated__/CheckInitialSetup' +import { useForm } from 'react-hook-form' +import { Submit, TextField } from '../../primitives/form/Input' +import MessageBox from '../../primitives/form/MessageBox' +import { + InitialSetup, + InitialSetupVariables, +} from './__generated__/InitialSetup' + +const initialSetupMutation = gql` + mutation InitialSetup( + $username: String! + $password: String! + $rootPath: String! + ) { + initialSetupWizard( + username: $username + password: $password + rootPath: $rootPath + ) { + success + status + token + } + } +` + +type InitialSetupFormData = { + username: string + password: string + rootPath: string +} + +const InitialSetupPage = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + const { + register, + handleSubmit, + formState: { errors: formErrors }, + } = useForm() + + useEffect(() => { + if (authToken()) navigate('/') + }, []) + + const { data: initialSetupData } = + useQuery(INITIAL_SETUP_QUERY) + + const notInitialSetup = initialSetupData?.siteInfo?.initialSetup === false + + useEffect(() => { + if (notInitialSetup) navigate('/') + }, [notInitialSetup]) + + const [authorize, { loading: authorizeLoading, data: authorizationData }] = + useMutation(initialSetupMutation, { + onCompleted: data => { + if (!data.initialSetupWizard) return + + const { success, token } = data.initialSetupWizard + if (success && token) login(token) + }, + }) + + const signIn = handleSubmit(data => { + authorize({ + variables: { + username: data.username, + password: data.password, + rootPath: data.rootPath, + }, + }) + }) + + if (authToken() || notInitialSetup) { + return null + } + + let errorMessage = null + if (authorizationData && !authorizationData?.initialSetupWizard?.success) { + errorMessage = authorizationData?.initialSetupWizard?.status + } + + return ( +
+ +

+ {t('login_page.initial_setup.title', 'Initial Setup')} +

+
+ + + + + + {t('login_page.initial_setup.field.submit', 'Setup Photoview')} + + +
+
+ ) +} + +export default InitialSetupPage diff --git a/ui/src/Pages/LoginPage/LoginPage.test.tsx b/ui/src/Pages/LoginPage/LoginPage.test.tsx new file mode 100644 index 0000000..8e8a0ef --- /dev/null +++ b/ui/src/Pages/LoginPage/LoginPage.test.tsx @@ -0,0 +1,76 @@ +import { render, screen, waitFor } from '@testing-library/react' +import React from 'react' +import LoginPage from './LoginPage' +import * as authentication from '../../helpers/authentication' +import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom' +import { createMemoryHistory } from 'history' +import { MockedProvider } from '@apollo/client/testing' +import { mockInitialSetupGraphql } from './loginTestHelpers' + +vi.mock('../../helpers/authentication.ts') + +const authToken = vi.mocked(authentication.authToken) + +describe('Login page redirects', () => { + test('Auth token redirect', async () => { + authToken.mockImplementation(() => 'some-token') + + const history = createMemoryHistory({ + initialEntries: ['/login'], + }) + + render( + + + + + + ) + + await waitFor(() => { + expect(history.location.pathname).toBe('/') + }) + }) + + test('Initial setup redirect', async () => { + authToken.mockImplementation(() => null) + + const history = createMemoryHistory({ + initialEntries: ['/login'], + }) + + render( + + + + + + ) + + await waitFor(() => { + expect(history.location.pathname).toBe('/initialSetup') + }) + }) +}) + +describe('Login page', () => { + test('Render login form', () => { + authToken.mockImplementation(() => null) + + const history = createMemoryHistory({ + initialEntries: ['/login'], + }) + + render( + + + + + + ) + + expect(screen.getByLabelText('Username')).toBeInTheDocument() + expect(screen.getByLabelText('Password')).toBeInTheDocument() + expect(screen.getByDisplayValue('Sign in')).toBeInTheDocument() + }) +}) diff --git a/ui/src/Pages/LoginPage/LoginPage.tsx b/ui/src/Pages/LoginPage/LoginPage.tsx new file mode 100644 index 0000000..62a0080 --- /dev/null +++ b/ui/src/Pages/LoginPage/LoginPage.tsx @@ -0,0 +1,155 @@ +import React, { useEffect } from 'react' +import { useQuery, gql, useMutation } from '@apollo/client' +import { useForm } from 'react-hook-form' +import { INITIAL_SETUP_QUERY, login } from './loginUtilities' +import { authToken } from '../../helpers/authentication' + +import { useTranslation } from 'react-i18next' +import { Helmet } from 'react-helmet' +import { useNavigate } from 'react-router' +import { TextField } from '../../primitives/form/Input' +import MessageBox from '../../primitives/form/MessageBox' +import { CheckInitialSetup } from './__generated__/CheckInitialSetup' +import { Authorize, AuthorizeVariables } from './__generated__/Authorize' + +const authorizeMutation = gql` + mutation Authorize($username: String!, $password: String!) { + authorizeUser(username: $username, password: $password) { + success + status + token + } + } +` + +const LogoHeader = () => { + const { t } = useTranslation() + + return ( +
+ photoview logo +

+ {t('login_page.welcome', 'Welcome to Photoview')} +

+
+ ) +} + +const LoginForm = () => { + const { t } = useTranslation() + const { + register, + handleSubmit, + formState: { errors: formErrors }, + } = useForm() + + const [authorize, { loading, data }] = useMutation< + Authorize, + AuthorizeVariables + >(authorizeMutation, { + onCompleted: data => { + const { success, token } = data.authorizeUser + + if (success && token) { + login(token) + } + }, + }) + + const onSubmit = (data: LoginInputs) => { + authorize({ + variables: { + username: data.username, + password: data.password, + }, + }) + } + + const errorMessage = + data && !data.authorizeUser.success ? data.authorizeUser.status : null + + return ( +
+ + + + + + ) +} + +type LoginInputs = { + username: string + password: string +} + +const LoginPage = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + const { data: initialSetupData } = useQuery( + INITIAL_SETUP_QUERY, + { variables: {} } + ) + + useEffect(() => { + if (authToken()) navigate('/') + }, []) + + useEffect(() => { + if (initialSetupData?.siteInfo?.initialSetup) navigate('/initialSetup') + }, [initialSetupData?.siteInfo?.initialSetup]) + + if (authToken() || initialSetupData?.siteInfo?.initialSetup) { + return null + } + + return ( + <> + + {t('title.login', 'Login')} - Photoview + +
+ + +
+ + ) +} + +export default LoginPage diff --git a/ui/src/Pages/LoginPage/__generated__/Authorize.ts b/ui/src/Pages/LoginPage/__generated__/Authorize.ts new file mode 100644 index 0000000..e5ab490 --- /dev/null +++ b/ui/src/Pages/LoginPage/__generated__/Authorize.ts @@ -0,0 +1,33 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: Authorize +// ==================================================== + +export interface Authorize_authorizeUser { + __typename: "AuthorizeResult"; + success: boolean; + /** + * A textual status message describing the result, can be used to show an error message when `success` is false + */ + status: string; + /** + * An access token used to authenticate new API requests as the newly authorized user. Is present when success is true + */ + token: string | null; +} + +export interface Authorize { + /** + * Authorizes a user and returns a token used to identify the new session + */ + authorizeUser: Authorize_authorizeUser; +} + +export interface AuthorizeVariables { + username: string; + password: string; +} diff --git a/ui/src/Pages/LoginPage/__generated__/CheckInitialSetup.ts b/ui/src/Pages/LoginPage/__generated__/CheckInitialSetup.ts new file mode 100644 index 0000000..94fe677 --- /dev/null +++ b/ui/src/Pages/LoginPage/__generated__/CheckInitialSetup.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: CheckInitialSetup +// ==================================================== + +export interface CheckInitialSetup_siteInfo { + __typename: "SiteInfo"; + /** + * Whether or not the initial setup wizard should be shown + */ + initialSetup: boolean; +} + +export interface CheckInitialSetup { + siteInfo: CheckInitialSetup_siteInfo; +} diff --git a/ui/src/Pages/LoginPage/__generated__/InitialSetup.ts b/ui/src/Pages/LoginPage/__generated__/InitialSetup.ts new file mode 100644 index 0000000..6a32a7d --- /dev/null +++ b/ui/src/Pages/LoginPage/__generated__/InitialSetup.ts @@ -0,0 +1,34 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: InitialSetup +// ==================================================== + +export interface InitialSetup_initialSetupWizard { + __typename: "AuthorizeResult"; + success: boolean; + /** + * A textual status message describing the result, can be used to show an error message when `success` is false + */ + status: string; + /** + * An access token used to authenticate new API requests as the newly authorized user. Is present when success is true + */ + token: string | null; +} + +export interface InitialSetup { + /** + * Registers the initial user, can only be called if initialSetup from SiteInfo is true + */ + initialSetupWizard: InitialSetup_initialSetupWizard | null; +} + +export interface InitialSetupVariables { + username: string; + password: string; + rootPath: string; +} diff --git a/ui/src/Pages/LoginPage/loginTestHelpers.ts b/ui/src/Pages/LoginPage/loginTestHelpers.ts new file mode 100644 index 0000000..cde4ecf --- /dev/null +++ b/ui/src/Pages/LoginPage/loginTestHelpers.ts @@ -0,0 +1,15 @@ +import { INITIAL_SETUP_QUERY } from './loginUtilities' + +export const mockInitialSetupGraphql = (initial: boolean) => ({ + request: { + query: INITIAL_SETUP_QUERY, + variables: {}, + }, + result: { + data: { + siteInfo: { + initialSetup: initial, + }, + }, + }, +}) diff --git a/ui/src/Pages/LoginPage/loginUtilities.tsx b/ui/src/Pages/LoginPage/loginUtilities.tsx new file mode 100644 index 0000000..5b53b3b --- /dev/null +++ b/ui/src/Pages/LoginPage/loginUtilities.tsx @@ -0,0 +1,18 @@ +import { gql } from '@apollo/client' +import { saveTokenCookie } from '../../helpers/authentication' +import styled from 'styled-components' + +export const INITIAL_SETUP_QUERY = gql` + query CheckInitialSetup { + siteInfo { + initialSetup + } + } +` + +export function login(token: string) { + saveTokenCookie(token) + window.location.href = `${import.meta.env.BASE_URL}` +} + +export const Container = styled.div.attrs({ className: 'mt-20' })`` diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx new file mode 100644 index 0000000..99c6389 --- /dev/null +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import FaceCircleImage from './FaceCircleImage' +import { myFaces_myFaceGroups_imageFaces } from './__generated__/myFaces' + +test('face circle image', () => { + const imageFace: myFaces_myFaceGroups_imageFaces = { + __typename: 'ImageFace', + id: '3', + media: { + id: '1', + __typename: 'Media', + title: 'my_image.jpg', + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_my_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + }, + }, + rectangle: { + __typename: 'FaceRectangle', + minX: 0.27, + maxX: 0.34, + minY: 0.76, + maxY: 0.88, + }, + } + + render() + + expect(screen.getByRole('img')).toBeInTheDocument() + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + 'http://localhost:3000/photo/thumbnail_my_image_jpg_p9x8dLWr.jpg' + ) +}) diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx new file mode 100644 index 0000000..f732aae --- /dev/null +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import styled from 'styled-components' +import { + ProtectedImage, + ProtectedImageProps, +} from '../../components/photoGallery/ProtectedMedia' +import { + myFaces_myFaceGroups_imageFaces_media, + myFaces_myFaceGroups_imageFaces_rectangle, +} from './__generated__/myFaces' + +type FaceImageProps = ProtectedImageProps & { + origin: { x: number; y: number } + selectable: boolean + scale: number +} + +const FaceImage = styled( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ origin, selectable, scale, ...rest }: FaceImageProps) => ( + + ) +)` + position: absolute; + transform-origin: ${({ origin }) => `${origin.x * 100}% ${origin.y * 100}%`}; + object-fit: cover; + + transition: transform 250ms ease-out; +` + +const FaceImagePortrait = styled(FaceImage)` + width: 100%; + top: 50%; + transform: translateY(-50%) + ${({ origin, scale }) => + `translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.8, 1)})`}; + + ${({ selectable, origin, scale }) => + selectable + ? ` + &:hover { + transform: translateY(-50%) translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.85, 1)}) + ` + : ''} +` + +const FaceImageLandscape = styled(FaceImage)` + height: 100%; + left: 50%; + transform: translateX(-50%) + ${({ origin, scale }) => + `translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.8, 1)})`}; + + ${({ selectable, origin, scale }) => + selectable + ? ` + &:hover { + transform: translateX(-50%) translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.85, 1)}) + ` + : ''} +` + +const CircleImageWrapper = styled.div<{ size: string }>` + background-color: #eee; + position: relative; + border-radius: 50%; + width: ${({ size }) => size}; + height: ${({ size }) => size}; + object-fit: fill; + overflow: hidden; +` + +type FaceCircleImageFace = { + __typename: 'ImageFace' + id: string + rectangle: myFaces_myFaceGroups_imageFaces_rectangle + media: myFaces_myFaceGroups_imageFaces_media +} + +type FaceCircleImageProps = { + imageFace: FaceCircleImageFace + selectable: boolean + size?: string +} + +const FaceCircleImage = ({ + imageFace, + selectable, + size = '150px', +}: FaceCircleImageProps) => { + if (!imageFace) { + return null + } + + const rect = imageFace.rectangle + + const scale = Math.min( + 1 / (rect.maxX - rect.minX), + 1 / (rect.maxY - rect.minY) + ) + + const origin = { + x: (rect.minX + rect.maxX) / 2, + y: (rect.minY + rect.maxY) / 2, + } + + let SpecificFaceImage: typeof FaceImageLandscape | typeof FaceImagePortrait = + FaceImageLandscape + if (imageFace.media.thumbnail) { + SpecificFaceImage = + imageFace.media.thumbnail.width > imageFace.media.thumbnail.height + ? FaceImageLandscape + : FaceImagePortrait + } + + return ( + + + + ) +} + +export default FaceCircleImage diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx new file mode 100644 index 0000000..ffeeaba --- /dev/null +++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx @@ -0,0 +1,246 @@ +import React from 'react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { + PeoplePage, + FaceDetails, + FaceGroup, + MY_FACES_QUERY, + SET_GROUP_LABEL_MUTATION, +} from './PeoplePage' +import { MockedProvider } from '@apollo/client/testing' +import { MemoryRouter } from 'react-router' +import { myFaces_myFaceGroups } from './__generated__/myFaces' + +vi.mock('../../hooks/useScrollPagination') + +describe('PeoplePage component', () => { + const graphqlMocks = [ + { + request: { + query: MY_FACES_QUERY, + variables: { + limit: 50, + offset: 0, + }, + }, + result: { + data: { + myFaceGroups: [ + { + __typename: 'FaceGroup', + id: '3', + label: 'Person A', + imageFaceCount: 2, + imageFaces: [ + { + __typename: 'ImageFace', + id: '3', + rectangle: { + __typename: 'FaceRectangle', + minX: 0.2705079913139343, + maxX: 0.3408200144767761, + minY: 0.7691109776496887, + maxY: 0.881434977054596, + }, + media: { + __typename: 'Media', + id: '63', + title: 'image.jpg', + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + }, + }, + }, + ], + }, + { + __typename: 'FaceGroup', + id: '1', + label: 'Person B', + imageFaceCount: 1, + imageFaces: [], + }, + ], + }, + }, + }, + ] + + test('people page', async () => { + render( + + + + + + ) + + expect(screen.getByTestId('Layout')).toBeInTheDocument() + expect(screen.getByText('Recognize unlabeled faces')).toBeInTheDocument() + + await waitFor(() => { + expect(screen.getByText('Person A')).toBeInTheDocument() + expect(screen.getByText('Person B')).toBeInTheDocument() + }) + + expect( + screen + .getAllByRole('link') + .some(x => x.getAttribute('href') == '/people/1') + ).toBeTruthy() + + expect( + screen + .getAllByRole('link') + .some(x => x.getAttribute('href') == '/people/3') + ).toBeTruthy() + }) +}) + +describe('FaceDetails component', () => { + const faceGroup: myFaces_myFaceGroups = { + id: '3', + label: null, + imageFaceCount: 2, + imageFaces: [ + { + id: '3', + rectangle: { + minX: 0.2705079913139343, + maxX: 0.3408200144767761, + minY: 0.7691109776496887, + maxY: 0.881434977054596, + __typename: 'FaceRectangle', + }, + media: { + id: '63', + title: 'image.jpg', + thumbnail: { + url: '/photo/thumbnail_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + __typename: 'MediaURL', + }, + __typename: 'Media', + }, + __typename: 'ImageFace', + }, + ], + __typename: 'FaceGroup', + } + + test('unlabeled, no images', () => { + const emptyFaceGroup: myFaces_myFaceGroups = { + ...faceGroup, + imageFaces: [], + } + + render( + + + + ) + + expect(screen.getByText('Unlabeled')).toBeInTheDocument() + }) + + test('labeled, with thumbnail', () => { + const labeledFaceGroup: myFaces_myFaceGroups = { + ...faceGroup, + label: 'Some label', + } + + render( + + + + ) + + expect(screen.getByText(labeledFaceGroup.label!)).toBeInTheDocument() + expect(screen.queryByText('Unlabeled')).not.toBeInTheDocument() + }) + + test('add label to face group', async () => { + const graphqlMocks = [ + { + request: { + query: SET_GROUP_LABEL_MUTATION, + variables: { + groupID: '3', + label: 'John Doe', + }, + }, + newData: vi.fn(() => ({ + data: { + setFaceGroupLabel: { + __typename: 'FaceGroup', + id: '3', + label: 'John Doe', + }, + }, + })), + }, + ] + render( + + + + + + ) + + const btn = screen.getByRole('button') + expect(btn).toBeInTheDocument() + expect(screen.queryByRole('textbox')).not.toBeInTheDocument() + + fireEvent.click(btn) + + const input = screen.getByRole('textbox') + expect(input).toBeInTheDocument() + expect(input).toHaveValue('') + + fireEvent.change(input, { target: { value: 'John Doe' } }) + fireEvent.keyUp(input, { key: 'Enter', code: 'Enter' }) + + await waitFor(() => { + expect(graphqlMocks[0].newData).toHaveBeenCalled() + }) + }) + + test('cancel add label to face group', () => { + render( + + + + + + ) + + const btn = screen.getByRole('button') + expect(btn).toBeInTheDocument() + expect(screen.queryByRole('textbox')).not.toBeInTheDocument() + expect(screen.queryByText('Unlabeled')).toBeInTheDocument() + + fireEvent.click(btn) + + const input = screen.getByRole('textbox') + expect(input).toBeInTheDocument() + expect(input).toHaveValue('') + + fireEvent.change(input, { target: { value: 'John Doe' } }) + fireEvent.keyDown(input, { key: 'Escape', code: 'Escape' }) + + expect(screen.queryByText('Unlabeled')).toBeInTheDocument() + }) +}) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.tsx b/ui/src/Pages/PeoplePage/PeoplePage.tsx new file mode 100644 index 0000000..2f06f54 --- /dev/null +++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx @@ -0,0 +1,352 @@ +import React, { createRef, useEffect, useState } from 'react' +import { gql, useMutation, useQuery } from '@apollo/client' +import Layout from '../../components/layout/Layout' +import styled from 'styled-components' +import { Link, useParams } from 'react-router-dom' +import SingleFaceGroup from './SingleFaceGroup/SingleFaceGroup' +import { Button, TextField } from '../../primitives/form/Input' +import FaceCircleImage from './FaceCircleImage' +import useScrollPagination from '../../hooks/useScrollPagination' +import PaginateLoader from '../../components/PaginateLoader' +import { useTranslation } from 'react-i18next' +import { + setGroupLabel, + setGroupLabelVariables, +} from './__generated__/setGroupLabel' +import { + myFaces, + myFacesVariables, + myFaces_myFaceGroups, +} from './__generated__/myFaces' +import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces' +import { isNil, tailwindClassNames } from '../../helpers/utils' +import classNames from 'classnames' +import MergeFaceGroupsModal, { + MergeFaceGroupsModalState, +} from './SingleFaceGroup/MergeFaceGroupsModal' + +export const MY_FACES_QUERY = gql` + query myFaces($limit: Int, $offset: Int) { + myFaceGroups(paginate: { limit: $limit, offset: $offset }) { + id + label + imageFaceCount + imageFaces(paginate: { limit: 1 }) { + id + rectangle { + minX + maxX + minY + maxY + } + media { + id + title + thumbnail { + url + width + height + } + } + } + } + } +` + +export const SET_GROUP_LABEL_MUTATION = gql` + mutation setGroupLabel($groupID: ID!, $label: String) { + setFaceGroupLabel(faceGroupID: $groupID, label: $label) { + id + label + } + } +` + +const RECOGNIZE_UNLABELED_FACES_MUTATION = gql` + mutation recognizeUnlabeledFaces { + recognizeUnlabeledFaces { + id + } + } +` + +type FaceDetailsWrapperProps = { + labeled: boolean + className?: string +} & React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLSpanElement +> + +const FaceDetailsWrapperInner = ({ + labeled, + children, + className, + ...otherProps +}: FaceDetailsWrapperProps) => ( + + {children} + +) + +const FaceDetailsWrapper = styled(FaceDetailsWrapperInner)` + &:hover, + &:focus-visible { + color: #2683ca; + } +` + +type FaceDetailsProps = { + group: { + __typename: 'FaceGroup' + id: string + label: string | null + imageFaceCount: number + } + className?: string + textFieldClassName?: string + editLabel: boolean + setEditLabel: React.Dispatch> +} + +export const FaceDetails = ({ + group, + className, + textFieldClassName, + editLabel, + setEditLabel, +}: FaceDetailsProps) => { + const { t } = useTranslation() + const [inputValue, setInputValue] = useState(group.label ?? '') + const inputRef = createRef() + + const [setGroupLabel, { loading }] = useMutation< + setGroupLabel, + setGroupLabelVariables + >(SET_GROUP_LABEL_MUTATION, { + variables: { + groupID: group.id, + }, + }) + + const resetLabel = () => { + setInputValue(group.label ?? '') + setEditLabel(false) + } + + useEffect(() => { + inputRef.current?.focus() + }, [inputRef]) + + useEffect(() => { + if (!loading) { + resetLabel() + } + }, [loading]) + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key == 'Escape') { + resetLabel() + return + } + } + + let label + if (!editLabel) { + label = ( + setEditLabel(true)} + > + {group.imageFaceCount} + + {/* */} + + ) + } else { + label = ( + + + setGroupLabel({ + variables: { + groupID: group.id, + label: inputValue == '' ? null : inputValue, + }, + }) + } + onKeyDown={onKeyDown} + onChange={e => setInputValue(e.target.value)} + onBlur={() => { + resetLabel() + }} + /> + + ) + } + + return label +} + +const FaceImagesCount = styled.span.attrs({ + className: + 'bg-gray-100 text-gray-900 dark:bg-gray-400 dark:text-black text-sm px-1 mr-2 rounded-md', +})`` + +type FaceGroupProps = { + group: myFaces_myFaceGroups +} + +export const FaceGroup = ({ group }: FaceGroupProps) => { + const previewFace = group.imageFaces[0] + const [editLabel, setEditLabel] = useState(false) + + return ( +
+ + + + +
+ ) +} + +const FaceGroupsWrapper = styled.div` + display: flex; + flex-wrap: wrap; + margin-top: 24px; +` + +/** + * Renders a page that shows a gallery of all people + */ +export const PeoplePage = () => { + const { t } = useTranslation() + const { data, error, loading, fetchMore } = useQuery< + myFaces, + myFacesVariables + >(MY_FACES_QUERY, { + variables: { + limit: 50, + offset: 0, + }, + }) + + const [mergeModalState, setMergeModalState] = useState( + MergeFaceGroupsModalState.Closed + ) + + const [recognizeUnlabeled, { loading: recognizeUnlabeledLoading }] = + useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION) + + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.myFaceGroups, + }) + + if (error) { + return
{error.message}
+ } + + let faces: JSX.Element[] | null = null + if (data) { + faces = data.myFaceGroups.map(faceGroup => ( + + )) + } + + let modals = null + modals = ( + + ) + + return ( + +
    +
  • + +
  • +
  • + +
  • +
+ + {faces} + + {modals} +
+ ) +} + +/** + * Renders a page for an individual person + */ +export const PersonPage = () => { + const { t } = useTranslation() + const { person } = useParams() + if (isNil(person)) + throw new Error('Expected `person` parameter to be defined') + + return ( + + + + ) +} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx new file mode 100644 index 0000000..575dd8c --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx @@ -0,0 +1,156 @@ +import { BaseMutationOptions, gql, useMutation } from '@apollo/client' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import { isNil } from '../../../helpers/utils' +import Modal from '../../../primitives/Modal' +import { MY_FACES_QUERY } from '../PeoplePage' +import { + myFaces_myFaceGroups, + myFaces_myFaceGroups_imageFaces, +} from '../__generated__/myFaces' +import SelectImageFacesTable from './SelectImageFacesTable' +import { + detachImageFaces, + detachImageFacesVariables, +} from './__generated__/detachImageFaces' +import { + singleFaceGroup_faceGroup, + singleFaceGroup_faceGroup_imageFaces, +} from './__generated__/singleFaceGroup' + +const DETACH_IMAGE_FACES_MUTATION = gql` + mutation detachImageFaces($faceIDs: [ID!]!) { + detachImageFaces(imageFaceIDs: $faceIDs) { + id + label + } + } +` + +export const useDetachImageFaces = ( + mutationOptions: BaseMutationOptions< + detachImageFaces, + detachImageFacesVariables + > +) => { + const [detachImageFacesMutation] = useMutation< + detachImageFaces, + detachImageFacesVariables + >(DETACH_IMAGE_FACES_MUTATION, mutationOptions) + + return async ( + selectedImageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + ) => { + const faceIDs = selectedImageFaces.map(face => face.id) + + const result = await detachImageFacesMutation({ + variables: { + faceIDs, + }, + }) + + return result + } +} + +type DetachImageFacesModalProps = { + open: boolean + setOpen(open: boolean): void + faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup + selectedImageFaces?: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] +} + +const DetachImageFacesModal = ({ + open, + setOpen, + faceGroup, + selectedImageFaces: selectedImageFacesProp, +}: DetachImageFacesModalProps) => { + const { t } = useTranslation() + + const [selectedImageFaces, setSelectedImageFaces] = useState< + (myFaces_myFaceGroups_imageFaces | singleFaceGroup_faceGroup_imageFaces)[] + >([]) + const navigate = useNavigate() + + const detachImageFacesMutation = useDetachImageFaces({ + refetchQueries: [ + { + query: MY_FACES_QUERY, + }, + ], + }) + + const detachImageFaces = () => { + detachImageFacesMutation(selectedImageFaces).then(({ data }) => { + if (isNil(data)) throw new Error('Expected data not to be null') + setOpen(false) + navigate(`/people/${data.detachImageFaces.id}`) + }) + } + + useEffect(() => { + if (isNil(selectedImageFacesProp)) return + setSelectedImageFaces(selectedImageFacesProp) + }, [selectedImageFacesProp]) + + useEffect(() => { + if (!open) { + setSelectedImageFaces([]) + } + }, [open]) + + if (open == false) return null + + const imageFaces = faceGroup?.imageFaces ?? [] + + return ( + setOpen(false), + }, + { + key: 'detach', + label: t( + 'people_page.modal.detach_image_faces.action.detach', + 'Detach image faces' + ), + variant: 'positive', + onClick: () => detachImageFaces(), + }, + ]} + onClose={() => setOpen(false)} + open={open} + > + + + ) +} + +export default DetachImageFacesModal diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx new file mode 100644 index 0000000..1b97d13 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx @@ -0,0 +1,175 @@ +import { useMutation } from '@apollo/client' +import React, { + useState, + useEffect, + createRef, + KeyboardEventHandler, +} from 'react' +import { useTranslation } from 'react-i18next' +import { isNil } from '../../../helpers/utils' +import { Button, TextField } from '../../../primitives/form/Input' +import { MY_FACES_QUERY, SET_GROUP_LABEL_MUTATION } from '../PeoplePage' +import { + setGroupLabel, + setGroupLabelVariables, +} from '../__generated__/setGroupLabel' +import DetachImageFacesModal from './DetachImageFacesModal' +import MergeFaceGroupsModal, { + MergeFaceGroupsModalState, +} from './MergeFaceGroupsModal' +import MoveImageFacesModal from './MoveImageFacesModal' +import { singleFaceGroup_faceGroup } from './__generated__/singleFaceGroup' + +type FaceGroupTitleProps = { + faceGroup?: singleFaceGroup_faceGroup +} + +const FaceGroupTitle = ({ faceGroup }: FaceGroupTitleProps) => { + const { t } = useTranslation() + + const [editLabel, setEditLabel] = useState(false) + const [inputValue, setInputValue] = useState(faceGroup?.label ?? '') + const inputRef = createRef() + const [mergeModalState, setMergeModalState] = useState( + MergeFaceGroupsModalState.Closed + ) + const [moveModalOpen, setMoveModalOpen] = useState(false) + const [detachModalOpen, setDetachModalOpen] = useState(false) + + const [setGroupLabel, { loading: setLabelLoading }] = useMutation< + setGroupLabel, + setGroupLabelVariables + >(SET_GROUP_LABEL_MUTATION) + + const resetLabel = () => { + setInputValue(faceGroup?.label ?? '') + setEditLabel(false) + } + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, [inputRef]) + + useEffect(() => { + if (!setLabelLoading) { + resetLabel() + } + }, [setLabelLoading]) + + const onKeyDown: KeyboardEventHandler = e => { + if (e.key == 'Escape') { + resetLabel() + return + } + } + + let title + if (!editLabel) { + title = ( + <> +

+ {faceGroup?.label ?? + t('people_page.face_group.unlabeled_person', 'Unlabeled person')} +

+ + ) + } else { + title = ( + <> + { + if (isNil(faceGroup)) + throw new Error('Expected faceGroup to be defined') + + setGroupLabel({ + variables: { + groupID: faceGroup.id, + label: inputValue ? inputValue : null, + }, + }) + }} + value={inputValue} + onKeyDown={onKeyDown} + onChange={e => setInputValue(e.target.value)} + onBlur={() => { + resetLabel() + }} + /> + + ) + } + + let modals = null + if (faceGroup) { + modals = ( + <> + + + + + ) + } + + return ( + <> +
+
{title}
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ {modals} + + ) +} + +export default FaceGroupTitle diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.test.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.test.tsx new file mode 100644 index 0000000..8d2c8a6 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.test.tsx @@ -0,0 +1,144 @@ +import { vi } from 'vitest' +import { render, fireEvent, waitFor } from '@testing-library/react' +import MergeFaceGroupsModal, { MergeFaceGroupsModalState } from './MergeFaceGroupsModal' +import { MockedProvider } from '@apollo/client/testing' +import { MY_FACES_QUERY } from '../PeoplePage' +import { COMBINE_FACES_MUTATION } from './MergeFaceGroupsModal' + +// Mock useNavigate before any imports that use it +const navigate = vi.fn() +vi.mock('react-router-dom', async () => { + const actual: object = await vi.importActual('react-router-dom') + return { ...actual, useNavigate: () => navigate } +}) + +// Mock IntersectionObserver for tests +beforeAll(() => { + global.IntersectionObserver = class { + constructor() {} + observe() {} + unobserve() {} + disconnect() {} + } as any +}) + +// Helper function to convert face group ID to test ID +function idToTestId(id: string): string { + return `facegroup-${id}` +} + +// Mock SelectFaceGroupTable to simplify selection logic +vi.mock('./SelectFaceGroupTable', () => ({ + __esModule: true, + default: ({ faceGroups, selectedFaceGroups, toggleSelectedFaceGroup, title }: any) => ( +
+
{title}
+ {faceGroups.map((fg: any) => ( + + ))} +
+ ), +})) + +const mockFaceGroups = ["Alice", "Bob", "Charlie", "David", "Felix"].map((name, index) => { return { __typename: 'FaceGroup', id: index.toString(), label: name, imageFaceCount: 0, imageFaces: [] }}) + +const myFacesMock = { + request: { query: MY_FACES_QUERY }, + result: { data: { myFaceGroups: mockFaceGroups } }, +} + +function getCombineFacesMock(destinationID: string, sourceIDs: string[]) { + return { + request: { + query: COMBINE_FACES_MUTATION, + variables: { + destID: destinationID, + srcIDs: sourceIDs, + }, + }, + result: { + data: { + combineFaceGroups: { + id: destinationID, + __typename: 'FaceGroup', + }, + }, + }, + } +} + +// Tests the merging of source face groups into a destination face group +// Logic extracted to be used in several tests +async function testMerge(destinationID: string, sourceIDs: string[]) { + const setState = vi.fn() + + const destinationTestID: string = idToTestId(destinationID) + const sourceTestIDs: string[] = sourceIDs.map(idToTestId) + + const combineFacesMock = getCombineFacesMock(destinationID, sourceIDs) + + // Render modal in SelectDestination state + const { getByText, getByTestId, getByRole, rerender } = render( + + + + ) + + // Wait for face groups to load and select destination + await waitFor(() => getByTestId(destinationTestID)) + fireEvent.click(getByTestId(destinationTestID)) + + // Click "Next" to go to SelectSources + fireEvent.click(getByText(/Next/i)) + expect(setState).toHaveBeenCalledWith(MergeFaceGroupsModalState.SelectSources) + + // Rerender in SelectSources state + rerender( + + + + ) + + // Wait for source face groups to load + await waitFor(() => getByTestId(sourceTestIDs[0])) + + // Select multiple source face groups + for (const testID of sourceTestIDs) + fireEvent.click(getByTestId(testID)) + + // Click the Merge button + fireEvent.click(getByRole('button', { name: /Merge/i })) + + // Check that the modal closes and redirects to the destination face group + await waitFor(() => { + expect(setState).toHaveBeenCalledWith(MergeFaceGroupsModalState.Closed) + expect(navigate).toHaveBeenCalledWith(`/people/${destinationID}`) + }) +} + +test('merges a single source face group into a destination face group', async () => { + testMerge(mockFaceGroups[0].id, [mockFaceGroups[1].id]) +}) + +test('merges all source face groups into a destination face group', async () => { + testMerge(mockFaceGroups[0].id, mockFaceGroups.slice(1).map(fg => fg.id)) +}) + +test('merges multiple source face groups into a destination face group', async () => { + testMerge(mockFaceGroups[0].id, [mockFaceGroups[1], mockFaceGroups[2]].map(fg => fg.id)) +}) diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.tsx new file mode 100644 index 0000000..a850a9a --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.tsx @@ -0,0 +1,262 @@ +import { gql, PureQueryOptions, useMutation, useQuery } from '@apollo/client' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import { isNil } from '../../../helpers/utils' +import Modal, { ModalAction, ModalProps } from '../../../primitives/Modal' +import { MY_FACES_QUERY } from '../PeoplePage' +import { + myFaces, + myFacesVariables, + myFaces_myFaceGroups, +} from '../__generated__/myFaces' +import SelectFaceGroupTable from './SelectFaceGroupTable' +import { + combineFaces, + combineFacesVariables, +} from './__generated__/combineFaces' +import { singleFaceGroup_faceGroup } from './__generated__/singleFaceGroup' + +export const COMBINE_FACES_MUTATION = gql` + mutation combineFaces($destID: ID!, $srcIDs: [ID!]!) { + combineFaceGroups( + destinationFaceGroupID: $destID + sourceFaceGroupIDs: $srcIDs + ) { + id + } + } +` + +export enum MergeFaceGroupsModalState { + Closed = 'closed', + SelectDestination = 'select_destination', + SelectSources = 'select_sources', +} + +type MergeFaceGroupsModalProps = { + state: MergeFaceGroupsModalState + setState(state: MergeFaceGroupsModalState): void + preselectedDestinationFaceGroup?: { + __typename: 'FaceGroup' + id: string + } + refetchQueries: PureQueryOptions[] +} + +type StateContent = { + props: ModalProps + searchTitle: string +} + +const MergeFaceGroupsModal = ({ + state, + setState, + preselectedDestinationFaceGroup, + refetchQueries, +}: MergeFaceGroupsModalProps) => { + const { t } = useTranslation() + + const navigate = useNavigate() + const { data } = useQuery(MY_FACES_QUERY) + const [combineFacesMutation] = useMutation< + combineFaces, + combineFacesVariables + >(COMBINE_FACES_MUTATION, { + refetchQueries: refetchQueries, + }) + + // The destination face group + const [selectedDestinationFaceGroup, setSelectedDestinationFaceGroup] = + useState(null) + + // The set of currently selected face groups, on the modal page + const [selectedFaceGroups, setSelectedFaceGroups] = useState< + Set + >(new Set()) + + const addSelectedFaceGroup = ( + faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup | null + ) => setSelectedFaceGroups(prev => new Set(prev).add(faceGroup)) + const removeSelectedFaceGroup = ( + faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup | null + ) => { + setSelectedFaceGroups(prev => { + const s = new Set(prev) + s.delete(faceGroup) + return s + }) + } + + const setDestinationFaceGroup = ( + faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup | null + ) => { + if (isNil(faceGroup)) { + setSelectedFaceGroups(new Set()) + setSelectedDestinationFaceGroup(null) + return + } + + // Overwrite the selected face groups with a set containing only the selected group + setSelectedFaceGroups( + new Set().add( + faceGroup + ) + ) + + setSelectedDestinationFaceGroup(faceGroup) + } + + // Go straight to the sources page if a destination face group is preselected, using the preselection as the destination + useEffect(() => { + if (isNil(preselectedDestinationFaceGroup)) return + if (state != MergeFaceGroupsModalState.SelectDestination) return + + const destinationFaceGroup = data?.myFaceGroups.find( + x => x.id == preselectedDestinationFaceGroup?.id + ) + if (isNil(destinationFaceGroup)) return + + setDestinationFaceGroup(destinationFaceGroup) + }, [state, preselectedDestinationFaceGroup]) + + function handleFaceGroupToggled(newValue: myFaces_myFaceGroups | singleFaceGroup_faceGroup) { + switch (state) { + case MergeFaceGroupsModalState.SelectDestination: + setSelectedDestinationFaceGroup(newValue) + setSelectedFaceGroups(new Set([newValue])) + break + case MergeFaceGroupsModalState.SelectSources: + if (selectedFaceGroups.has(newValue)) + removeSelectedFaceGroup(newValue) + else addSelectedFaceGroup(newValue) + break + } + } + + // Show all face groups on the destination page, but filter out the destination group on the source page + const filteredFaceGroups = + data?.myFaceGroups.filter( + x => + state === MergeFaceGroupsModalState.SelectDestination || + x.id != selectedDestinationFaceGroup?.id + ) ?? [] + + const goNext = () => { + if (isNil(selectedDestinationFaceGroup)) + throw new Error('No selected face group') + + setState(MergeFaceGroupsModalState.SelectSources) + setSelectedFaceGroups(new Set()) + } + + const mergeFaceGroups = () => { + if (isNil(selectedDestinationFaceGroup)) + throw new Error('No selected destination face group') + + const sourceGroupIDs: string[] = [...selectedFaceGroups].filter(fc => fc !== null).map(fc => fc.id) + + if(sourceGroupIDs.length < 1) + throw new Error('No selected source face groups') + + combineFacesMutation({ + variables: { + srcIDs: sourceGroupIDs, + destID: selectedDestinationFaceGroup.id, + }, + }).then(() => { + setState(MergeFaceGroupsModalState.Closed) + navigate(`/people/${selectedDestinationFaceGroup.id}`) + }) + } + + const closeModal = () => { + setState(MergeFaceGroupsModalState.Closed) + } + + const isOpen: boolean = state !== MergeFaceGroupsModalState.Closed + + const cancelAction: ModalAction = { + key: 'cancel', + label: t('general.action.cancel', 'Cancel'), + onClick: () => setState(MergeFaceGroupsModalState.Closed), + } + + const nextAction: ModalAction = { + key: 'next', + label: t('people_page.modal.action.next', 'Next'), + onClick: () => goNext(), + variant: 'positive', + } + + const mergeAction: ModalAction = { + key: 'merge', + label: t('people_page.modal.action.merge', 'Merge'), + onClick: () => mergeFaceGroups(), + variant: 'positive', + } + + const modalTitle: string = t( + 'people_page.modal.merge_face_groups.title', + 'Merge Face Groups' + ) + + const selectDestinationProps: StateContent = { + props: { + title: modalTitle, + description: t( + 'people_page.modal.merge_face_groups.destination_description', + 'Select the face group that other groups should be merged into.' + ), + actions: [cancelAction, nextAction], + onClose: closeModal, + open: isOpen, + }, + searchTitle: t( + 'people_page.modal.merge_face_groups.destination_table.title', + 'Select the destination face' + ), + } + + const selectSourcesProps: StateContent = { + props: { + title: modalTitle, + description: t( + 'people_page.modal.merge_face_groups.sources_description', + 'Select all face groups that will be merged into the destination group.' + ), + actions: [cancelAction, mergeAction], + onClose: closeModal, + open: isOpen, + }, + searchTitle: + t( + 'people_page.modal.merge_face_groups.sources_table.title', + 'Select one or more source faces to merge into:' + ) + + ` ${ + selectedDestinationFaceGroup?.label ?? + t('people_page.face_group.unlabeled', 'Unlabeled') ?? + 'Unlabeled' + }`, + } + + const modalContent: StateContent = + state === MergeFaceGroupsModalState.SelectDestination + ? selectDestinationProps + : selectSourcesProps + + return ( + + { if(face === null) return; handleFaceGroupToggled(face) }} + /> + + ) +} + +export default MergeFaceGroupsModal diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx new file mode 100644 index 0000000..8947fa0 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx @@ -0,0 +1,204 @@ +import { gql, useLazyQuery, useMutation } from '@apollo/client' +import React, { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import SelectFaceGroupTable from './SelectFaceGroupTable' +import SelectImageFacesTable from './SelectImageFacesTable' +import { MY_FACES_QUERY } from '../PeoplePage' +import { + singleFaceGroup_faceGroup, + singleFaceGroup_faceGroup_imageFaces, +} from './__generated__/singleFaceGroup' +import { + myFaces, + myFacesVariables, + myFaces_myFaceGroups, + myFaces_myFaceGroups_imageFaces, +} from '../__generated__/myFaces' +import { isNil } from '../../../helpers/utils' +import { + moveImageFaces, + moveImageFacesVariables, +} from './__generated__/moveImageFaces' +import { useTranslation } from 'react-i18next' +import Modal, { ModalAction } from '../../../primitives/Modal' + +const MOVE_IMAGE_FACES_MUTATION = gql` + mutation moveImageFaces($faceIDs: [ID!]!, $destFaceGroupID: ID!) { + moveImageFaces( + imageFaceIDs: $faceIDs + destinationFaceGroupID: $destFaceGroupID + ) { + id + imageFaces { + id + } + } + } +` + +type MoveImageFacesModalProps = { + open: boolean + setOpen: React.Dispatch> + faceGroup: singleFaceGroup_faceGroup + preselectedImageFaces?: ( + | singleFaceGroup_faceGroup_imageFaces + | myFaces_myFaceGroups_imageFaces + )[] +} + +const MoveImageFacesModal = ({ + open, + setOpen, + faceGroup, + preselectedImageFaces, +}: MoveImageFacesModalProps) => { + const { t } = useTranslation() + + const [selectedImageFaces, setSelectedImageFaces] = useState< + (singleFaceGroup_faceGroup_imageFaces | myFaces_myFaceGroups_imageFaces)[] + >([]) + const [selectedFaceGroup, setSelectedFaceGroup] = useState< + myFaces_myFaceGroups | singleFaceGroup_faceGroup | null + >(null) + const [imagesSelected, setImagesSelected] = useState(false) + const navigate = useNavigate() + + const [moveImageFacesMutation] = useMutation< + moveImageFaces, + moveImageFacesVariables + >(MOVE_IMAGE_FACES_MUTATION, { + refetchQueries: [ + { + query: MY_FACES_QUERY, + }, + ], + }) + + const [loadFaceGroups, { data: faceGroupsData }] = useLazyQuery< + myFaces, + myFacesVariables + >(MY_FACES_QUERY) + + useEffect(() => { + if (isNil(preselectedImageFaces)) return + setSelectedImageFaces(preselectedImageFaces) + setImagesSelected(true) + }, [preselectedImageFaces]) + + useEffect(() => { + if (imagesSelected) { + loadFaceGroups() + } + }, [imagesSelected]) + + useEffect(() => { + if (!open) { + setImagesSelected(false) + setSelectedImageFaces([]) + setSelectedFaceGroup(null) + } + }, [open]) + + if (open == false) return null + + const moveImageFaces = () => { + const faceIDs = selectedImageFaces.map(face => face.id) + + if (isNil(selectedFaceGroup)) { + throw new Error('Expected selectedFaceGroup not to be null') + } + + moveImageFacesMutation({ + variables: { + faceIDs, + destFaceGroupID: selectedFaceGroup.id, + }, + }).then(() => { + setOpen(false) + navigate(`/people/${selectedFaceGroup.id}`) + }) + } + + const imageFaces = faceGroup.imageFaces + + let table = null + if (!imagesSelected) { + table = ( + + ) + } else { + if (faceGroupsData && faceGroup) { + const filteredFaceGroups = faceGroupsData.myFaceGroups.filter( + x => x.id != faceGroup.id + ) + table = ( + + ) + } else { + table =
{t('general.loading.default', 'Loading...')}
+ } + } + + let positiveButton: ModalAction + if (!imagesSelected) { + positiveButton = { + key: 'next', + label: t( + 'people_page.modal.move_image_faces.image_select_table.next_action', + 'Next' + ), + onClick: () => setImagesSelected(true), + variant: 'positive', + } + } else { + positiveButton = { + key: 'move', + label: t( + 'people_page.modal.move_image_faces.destination_face_group_table.move_action', + 'Move image faces' + ), + onClick: () => moveImageFaces(), + variant: 'positive', + } + } + + return ( + setOpen(false)} + open={open} + actions={[ + { + key: 'cancel', + label: t('general.action.cancel', 'Cancel'), + onClick: () => setOpen(false), + }, + positiveButton, + ]} + > + {table} + + ) +} + +export default MoveImageFacesModal diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx new file mode 100644 index 0000000..addf328 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx @@ -0,0 +1,142 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { TextField } from '../../../primitives/form/Input' +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from '../../../primitives/Table' +import FaceCircleImage from '../FaceCircleImage' +import { myFaces_myFaceGroups } from '../__generated__/myFaces' +import { singleFaceGroup_faceGroup } from './__generated__/singleFaceGroup' + +const FaceCircleWrapper = styled.div<{ $selected: boolean }>` + display: inline-block; + border-radius: 50%; + border: 2px solid + ${({ $selected }) => ($selected ? `#2185c9` : 'rgba(255,255,255,0)')}; +` + +const FlexCell = styled(TableCell)` + display: flex; + align-items: center; +` + +export const RowLabel = styled.span<{ $selected: boolean }>` + ${({ $selected }) => $selected && `font-weight: bold;`} + margin-left: 12px; + width: 100%; +` + +type FaceGroupRowProps = { + faceGroup: myFaces_myFaceGroups + faceSelected: boolean + selectable: boolean + toggleFaceSelected(): void +} + +const FaceGroupRow = ({ + faceGroup, + faceSelected, + selectable, + toggleFaceSelected, +}: FaceGroupRowProps) => { + const { t } = useTranslation() + return ( + + + + + + + {faceGroup.label ?? + t('people_page.face_group.unlabeled', 'Unlabeled')} + + + + ) +} + +type SelectFaceGroupTableProps = { + faceGroups: myFaces_myFaceGroups[] + selectedFaceGroups: Set< + singleFaceGroup_faceGroup | myFaces_myFaceGroups | null + > + toggleSelectedFaceGroup: React.Dispatch< + singleFaceGroup_faceGroup | myFaces_myFaceGroups | null + > + title: string, + frozen: boolean +} + +const SelectFaceGroupTable = ({ + faceGroups, + selectedFaceGroups, + toggleSelectedFaceGroup, + title, + frozen +}: SelectFaceGroupTableProps) => { + const { t } = useTranslation() + + const [searchValue, setSearchValue] = useState('') + + const rows = faceGroups + .filter( + x => + searchValue == '' || + (x.label && x.label.toLowerCase().includes(searchValue.toLowerCase())) + ) + .map(face => ( + val?.id === face.id)} + selectable={!frozen} + toggleFaceSelected={() => !frozen && toggleSelectedFaceGroup(face)} + /> + )) + + return ( + <> + + + + {title} + + + + setSearchValue(e.target.value)} + placeholder={t( + 'people_page.tableselect_face_group.search_faces_placeholder', + 'Search faces...' + )} + /> + + + +
+
+ + {rows} +
+
+ + ) +} + +export default SelectFaceGroupTable diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx new file mode 100644 index 0000000..360cd7d --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx @@ -0,0 +1,147 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { ProtectedImage } from '../../../components/photoGallery/ProtectedMedia' +import Checkbox from '../../../primitives/form/Checkbox' +import { TextField } from '../../../primitives/form/Input' +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from '../../../primitives/Table' +import { myFaces_myFaceGroups_imageFaces } from '../__generated__/myFaces' +import { singleFaceGroup_faceGroup_imageFaces } from './__generated__/singleFaceGroup' + +const SelectImagePreview = styled(ProtectedImage)` + max-width: 120px; + max-height: 80px; +` + +type ImageFaceRowProps = { + imageFace: + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + faceSelected: boolean + setFaceSelected(): void +} + +const ImageFaceRow = ({ + imageFace, + faceSelected, + setFaceSelected, +}: ImageFaceRowProps) => { + return ( + + + + + + + + + ) +} + +type SelectImageFacesTable = { + imageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + selectedImageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + setSelectedImageFaces: React.Dispatch< + React.SetStateAction< + (myFaces_myFaceGroups_imageFaces | singleFaceGroup_faceGroup_imageFaces)[] + > + > + title: string +} + +const SelectImageFacesTable = ({ + imageFaces, + selectedImageFaces, + setSelectedImageFaces, + title, +}: SelectImageFacesTable) => { + const { t } = useTranslation() + + // const PAGE_SIZE = 6 + + // const [page, setPage] = useState(0) + const [searchValue, setSearchValue] = useState('') + + // useEffect(() => { + // setPage(0) + // }, [searchValue]) + + const rows = imageFaces + .filter( + face => + searchValue == '' || + face.media.title.toLowerCase().includes(searchValue.toLowerCase()) + ) + .map(face => ( + + setSelectedImageFaces(faces => { + if (faces.includes(face)) { + return faces.filter(x => x != face) + } else { + return [...faces, face] + } + }) + } + /> + )) + + // const pageRows = rows.filter( + // (_, i) => i >= page * PAGE_SIZE && i < (page + 1) * PAGE_SIZE + // ) + + return ( + <> + + + + {title} + + + + setSearchValue(e.target.value)} + placeholder={t( + 'people_page.tableselect_image_faces.search_images_placeholder', + 'Search images...' + )} + fullWidth + /> + + + +
+
+ + {rows} +
+
+ + ) +} + +export default SelectImageFacesTable diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.test.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.test.tsx new file mode 100644 index 0000000..6b3f072 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.test.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { MockedProvider } from '@apollo/client/testing' +import SingleFaceGroup, { SINGLE_FACE_GROUP } from './SingleFaceGroup' +import { MemoryRouter } from 'react-router-dom' + +vi.mock('../../../hooks/useScrollPagination') + +test('single face group', async () => { + const graphqlMocks = [ + { + request: { + query: SINGLE_FACE_GROUP, + variables: { limit: 200, offset: 0, id: '123' }, + }, + result: { + data: { + faceGroup: { + __typename: 'FaceGroup', + id: '2', + label: 'Face Group Name', + imageFaces: [ + { + __typename: 'ImageFace', + id: '1', + rectangle: { + __typename: 'FaceRectangle', + minX: 0.4912109971046448, + maxX: 0.5927730202674866, + minY: 0.2998049855232239, + maxY: 0.4013670086860657, + }, + media: { + __typename: 'Media', + id: '10', + type: 'Photo', + title: '122A2785-2.jpg', + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_122A2785-2_jpg_lFmZcaN5.jpg', + width: 1024, + height: 1024, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/122A2785-2_e4nCeMHU.jpg', + }, + favorite: false, + }, + }, + { + __typename: 'ImageFace', + id: '2', + rectangle: { + __typename: 'FaceRectangle', + minX: 0.265625, + maxX: 0.3876950144767761, + minY: 0.1917019933462143, + maxY: 0.3705289959907532, + }, + media: { + __typename: 'Media', + id: '52', + type: 'Photo', + title: 'image.png', + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_image_png_OwTDG5fM.jpg', + width: 1024, + height: 699, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/image_A2YB0x3z.png', + }, + favorite: false, + }, + }, + ], + }, + }, + }, + }, + ] + + render( + + + + + + ) + + await waitFor(() => { + expect(screen.getAllByRole('img')).toHaveLength(2) + }) +}) diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx new file mode 100644 index 0000000..6f3aa20 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx @@ -0,0 +1,108 @@ +import { gql, useQuery } from '@apollo/client' +import React, { useEffect, useReducer } from 'react' +import { useTranslation } from 'react-i18next' +import PaginateLoader from '../../../components/PaginateLoader' +import MediaGallery from '../../../components/photoGallery/MediaGallery' +import { mediaGalleryReducer } from '../../../components/photoGallery/mediaGalleryReducer' +import useScrollPagination from '../../../hooks/useScrollPagination' +import FaceGroupTitle from './FaceGroupTitle' +import { + singleFaceGroup, + singleFaceGroupVariables, +} from './__generated__/singleFaceGroup' + +export const SINGLE_FACE_GROUP = gql` + query singleFaceGroup($id: ID!, $limit: Int!, $offset: Int!) { + faceGroup(id: $id) { + id + label + imageFaces(paginate: { limit: $limit, offset: $offset }) { + id + rectangle { + minX + maxX + minY + maxY + } + media { + id + type + title + blurhash + thumbnail { + url + width + height + } + highRes { + url + } + favorite + } + } + } + } +` + +type SingleFaceGroupProps = { + faceGroupID: string +} + +const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => { + const { t } = useTranslation() + + const { data, error, loading, fetchMore } = useQuery< + singleFaceGroup, + singleFaceGroupVariables + >(SINGLE_FACE_GROUP, { + variables: { + limit: 200, + offset: 0, + id: faceGroupID, + }, + }) + + const [mediaState, dispatchMedia] = useReducer(mediaGalleryReducer, { + presenting: false, + activeIndex: -1, + media: [], + }) + + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.faceGroup.imageFaces, + }) + + useEffect(() => { + const media = data?.faceGroup.imageFaces.map(x => x.media) || [] + dispatchMedia({ type: 'replaceMedia', media }) + }, [data]) + + const faceGroup = data?.faceGroup + + if (error) { + return
{error.message}
+ } + + return ( +
+ +
+ + +
+
+ ) +} + +export default SingleFaceGroup diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/combineFaces.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/combineFaces.ts new file mode 100644 index 0000000..63f6c5b --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/combineFaces.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: combineFaces +// ==================================================== + +export interface combineFaces_combineFaceGroups { + __typename: "FaceGroup"; + id: string; +} + +export interface combineFaces { + /** + * Merge two face groups into a single one, all ImageFaces from source will be moved to destination + */ + combineFaceGroups: combineFaces_combineFaceGroups; +} + +export interface combineFacesVariables { + destID: string; + srcIDs: string[]; +} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/detachImageFaces.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/detachImageFaces.ts new file mode 100644 index 0000000..644db86 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/detachImageFaces.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: detachImageFaces +// ==================================================== + +export interface detachImageFaces_detachImageFaces { + __typename: "FaceGroup"; + id: string; + /** + * The name of the person + */ + label: string | null; +} + +export interface detachImageFaces { + /** + * Move a list of ImageFaces to a new face group + */ + detachImageFaces: detachImageFaces_detachImageFaces; +} + +export interface detachImageFacesVariables { + faceIDs: string[]; +} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/moveImageFaces.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/moveImageFaces.ts new file mode 100644 index 0000000..1279f42 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/moveImageFaces.ts @@ -0,0 +1,31 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: moveImageFaces +// ==================================================== + +export interface moveImageFaces_moveImageFaces_imageFaces { + __typename: "ImageFace"; + id: string; +} + +export interface moveImageFaces_moveImageFaces { + __typename: "FaceGroup"; + id: string; + imageFaces: moveImageFaces_moveImageFaces_imageFaces[]; +} + +export interface moveImageFaces { + /** + * Move a list of ImageFaces to another face group + */ + moveImageFaces: moveImageFaces_moveImageFaces; +} + +export interface moveImageFacesVariables { + faceIDs: string[]; + destFaceGroupID: string; +} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/singleFaceGroup.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/singleFaceGroup.ts new file mode 100644 index 0000000..e714408 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/singleFaceGroup.ts @@ -0,0 +1,98 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: singleFaceGroup +// ==================================================== + +export interface singleFaceGroup_faceGroup_imageFaces_rectangle { + __typename: "FaceRectangle"; + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export interface singleFaceGroup_faceGroup_imageFaces_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface singleFaceGroup_faceGroup_imageFaces_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface singleFaceGroup_faceGroup_imageFaces_media { + __typename: "Media"; + id: string; + type: MediaType; + title: string; + /** + * A short string that can be used to generate a blured version of the media, to show while the original is loading + */ + blurhash: string | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: singleFaceGroup_faceGroup_imageFaces_media_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: singleFaceGroup_faceGroup_imageFaces_media_highRes | null; + favorite: boolean; +} + +export interface singleFaceGroup_faceGroup_imageFaces { + __typename: "ImageFace"; + id: string; + /** + * A bounding box of where on the image the face is present + */ + rectangle: singleFaceGroup_faceGroup_imageFaces_rectangle; + /** + * A reference to the image the face appears on + */ + media: singleFaceGroup_faceGroup_imageFaces_media; +} + +export interface singleFaceGroup_faceGroup { + __typename: "FaceGroup"; + id: string; + /** + * The name of the person + */ + label: string | null; + imageFaces: singleFaceGroup_faceGroup_imageFaces[]; +} + +export interface singleFaceGroup { + /** + * Get a particular `FaceGroup` specified by its ID + */ + faceGroup: singleFaceGroup_faceGroup; +} + +export interface singleFaceGroupVariables { + id: string; + limit: number; + offset: number; +} diff --git a/ui/src/Pages/PeoplePage/__generated__/myFaces.ts b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts new file mode 100644 index 0000000..3fbdc1a --- /dev/null +++ b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts @@ -0,0 +1,81 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: myFaces +// ==================================================== + +export interface myFaces_myFaceGroups_imageFaces_rectangle { + __typename: "FaceRectangle"; + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export interface myFaces_myFaceGroups_imageFaces_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface myFaces_myFaceGroups_imageFaces_media { + __typename: "Media"; + id: string; + title: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: myFaces_myFaceGroups_imageFaces_media_thumbnail | null; +} + +export interface myFaces_myFaceGroups_imageFaces { + __typename: "ImageFace"; + id: string; + /** + * A bounding box of where on the image the face is present + */ + rectangle: myFaces_myFaceGroups_imageFaces_rectangle; + /** + * A reference to the image the face appears on + */ + media: myFaces_myFaceGroups_imageFaces_media; +} + +export interface myFaces_myFaceGroups { + __typename: "FaceGroup"; + id: string; + /** + * The name of the person + */ + label: string | null; + /** + * The total number of images in this collection + */ + imageFaceCount: number; + imageFaces: myFaces_myFaceGroups_imageFaces[]; +} + +export interface myFaces { + /** + * Get a list of `FaceGroup`s for the logged in user + */ + myFaceGroups: myFaces_myFaceGroups[]; +} + +export interface myFacesVariables { + limit?: number | null; + offset?: number | null; +} diff --git a/ui/src/Pages/PeoplePage/__generated__/recognizeUnlabeledFaces.ts b/ui/src/Pages/PeoplePage/__generated__/recognizeUnlabeledFaces.ts new file mode 100644 index 0000000..bfbfb49 --- /dev/null +++ b/ui/src/Pages/PeoplePage/__generated__/recognizeUnlabeledFaces.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: recognizeUnlabeledFaces +// ==================================================== + +export interface recognizeUnlabeledFaces_recognizeUnlabeledFaces { + __typename: "ImageFace"; + id: string; +} + +export interface recognizeUnlabeledFaces { + /** + * Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match + */ + recognizeUnlabeledFaces: recognizeUnlabeledFaces_recognizeUnlabeledFaces[]; +} diff --git a/ui/src/Pages/PeoplePage/__generated__/setGroupLabel.ts b/ui/src/Pages/PeoplePage/__generated__/setGroupLabel.ts new file mode 100644 index 0000000..6aead82 --- /dev/null +++ b/ui/src/Pages/PeoplePage/__generated__/setGroupLabel.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: setGroupLabel +// ==================================================== + +export interface setGroupLabel_setFaceGroupLabel { + __typename: "FaceGroup"; + id: string; + /** + * The name of the person + */ + label: string | null; +} + +export interface setGroupLabel { + /** + * Assign a label to a face group, set label to null to remove the current one + */ + setFaceGroupLabel: setGroupLabel_setFaceGroupLabel; +} + +export interface setGroupLabelVariables { + groupID: string; + label?: string | null; +} diff --git a/ui/src/Pages/PlacesPage/MapClusterMarker.tsx b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx new file mode 100644 index 0000000..5911662 --- /dev/null +++ b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import styled from 'styled-components' +import imagePopupSrc from './image-popup.svg' +import { MediaMarker } from './MapPresentMarker' +import { PlacesAction } from './placesReducer' + +const Wrapper = styled.div` + width: 56px; + height: 68px; + position: relative; + margin-top: -54px; + cursor: pointer; +` + +const ThumbnailImage = styled.img` + position: absolute; + width: 48px; + height: 48px; + top: 4px; + left: 4px; + border-radius: 2px; + object-fit: cover; +` + +const PopupImage = styled.img` + width: 100%; + height: 100%; +` + +const PointCountCircle = styled.div` + position: absolute; + top: -10px; + right: -10px; + width: 24px; + height: 24px; + background-color: #00b3dc; + border-radius: 50%; + color: white; + text-align: center; + padding-top: 2px; +` + +type MapClusterMarkerProps = { + dispatchMarkerMedia: React.Dispatch + marker: MediaMarker +} + +const MapClusterMarker = ({ + marker, + dispatchMarkerMedia, +}: MapClusterMarkerProps) => { + const thumbnail = JSON.parse(marker.thumbnail) as { url: string } + + const presentMedia = () => { + dispatchMarkerMedia({ + type: 'replacePresentMarker', + marker: { + cluster: !!marker.cluster, + id: marker.cluster ? marker.cluster_id : marker.media_id, + }, + }) + } + + return ( + + + + {marker.cluster && ( + {marker.point_count_abbreviated} + )} + + ) +} + +export default MapClusterMarker diff --git a/ui/src/Pages/PlacesPage/MapPresentMarker.tsx b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx new file mode 100644 index 0000000..ce1a89a --- /dev/null +++ b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx @@ -0,0 +1,142 @@ +import { gql } from '@apollo/client' +import React, { useEffect } from 'react' +import { useLazyQuery } from '@apollo/client' +import PresentView from '../../components/photoGallery/presentView/PresentView' +import type mapboxgl from 'mapbox-gl' +import { PresentMarker } from './PlacesPage' +import { + placePageQueryMedia, + placePageQueryMediaVariables, +} from './__generated__/placePageQueryMedia' +import { PlacesAction, PlacesState } from './placesReducer' + +const QUERY_MEDIA = gql` + query placePageQueryMedia($mediaIDs: [ID!]!) { + mediaList(ids: $mediaIDs) { + id + title + blurhash + thumbnail { + url + width + height + } + highRes { + url + width + height + } + videoWeb { + url + width + height + } + type + } + } +` + +const getMediaFromMarker = (map: mapboxgl.Map, presentMarker: PresentMarker) => + new Promise((resolve, reject) => { + const { cluster, id } = presentMarker + + if (cluster) { + const mediaSource = map.getSource('media') as mapboxgl.GeoJSONSource + + mediaSource.getClusterLeaves(id as number, 1000, 0, (error, features) => { + if (error) { + reject(error) + return + } + + const media = features.map(feat => feat.properties) as MediaMarker[] + resolve(media) + }) + } else { + const features = map.querySourceFeatures('media') + const media = features.find(f => f.properties?.media_id == id) + ?.properties as MediaMarker | undefined + + if (media === undefined) { + reject('ERROR: media is undefined') + return + } + + resolve([media]) + } + }) + +export interface MediaMarker { + id: number + thumbnail: string + cluster: boolean + point_count_abbreviated: number + cluster_id: string + media_id: string +} + +type MapPresetMarkerProps = { + map: mapboxgl.Map | null + markerMediaState: PlacesState + dispatchMarkerMedia: React.Dispatch +} + +/** + * Full-screen present-view that works with PlacesState + */ +const MapPresentMarker = ({ + map, + markerMediaState, + dispatchMarkerMedia, +}: MapPresetMarkerProps) => { + const [loadMedia, { data: loadedMedia }] = useLazyQuery< + placePageQueryMedia, + placePageQueryMediaVariables + >(QUERY_MEDIA) + + useEffect(() => { + const presentMarker = markerMediaState.presentMarker + if (presentMarker == null || map == null) { + dispatchMarkerMedia({ + type: 'closePresentMode', + }) + return + } + + getMediaFromMarker(map, presentMarker).then(mediaMarkers => { + loadMedia({ + variables: { + mediaIDs: mediaMarkers.map(x => x.media_id), + }, + }) + }) + }, [markerMediaState.presentMarker]) + + useEffect(() => { + const mediaList = loadedMedia?.mediaList || [] + dispatchMarkerMedia({ + type: 'replaceMedia', + media: mediaList, + }) + if (mediaList.length > 0) { + dispatchMarkerMedia({ + type: 'openPresentMode', + activeIndex: 0, + }) + } + }, [loadedMedia]) + + if (markerMediaState.presenting) { + return ( + + ) + } else { + return null + } +} + +export default MapPresentMarker diff --git a/ui/src/Pages/PlacesPage/PlacesPage.tsx b/ui/src/Pages/PlacesPage/PlacesPage.tsx new file mode 100644 index 0000000..c76f778 --- /dev/null +++ b/ui/src/Pages/PlacesPage/PlacesPage.tsx @@ -0,0 +1,140 @@ +import { gql, useQuery } from '@apollo/client' +import type mapboxgl from 'mapbox-gl' +import React, { useReducer } from 'react' +import { Helmet } from 'react-helmet' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import Layout from '../../components/layout/Layout' +import { registerMediaMarkers } from '../../components/mapbox/mapboxHelperFunctions' +import useMapboxMap from '../../components/mapbox/MapboxMap' +import { urlPresentModeSetupHook } from '../../components/photoGallery/mediaGalleryReducer' +import MapPresentMarker from './MapPresentMarker' +import { PlacesAction, placesReducer } from './placesReducer' +import { mediaGeoJson } from './__generated__/mediaGeoJson' + +const MapWrapper = styled.div` + width: 100%; + height: calc(100vh - 120px); +` + +const MAPBOX_DATA_QUERY = gql` + query mediaGeoJson { + myMediaGeoJson + } +` + +export type PresentMarker = { + id: number | string + cluster: boolean +} + +const MapPage = () => { + const { t } = useTranslation() + + const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY, { + fetchPolicy: 'cache-first', + }) + + const [markerMediaState, dispatchMarkerMedia] = useReducer(placesReducer, { + presenting: false, + activeIndex: -1, + media: [], + }) + + const { mapContainer, mapboxMap, mapboxToken } = useMapboxMap({ + configureMapbox: configureMapbox({ mapboxData, dispatchMarkerMedia }), + mapboxOptions: { + projection: { + name: 'globe', + }, + }, + }) + + urlPresentModeSetupHook({ + dispatchMedia: dispatchMarkerMedia, + openPresentMode: event => { + dispatchMarkerMedia({ + type: 'openPresentMode', + activeIndex: event.state.activeIndex, + }) + }, + }) + + if (mapboxData && mapboxToken == null) { + return ( + +

Mapbox token is not set

+

+ To use map related features a mapbox token is needed. +
A mapbox token can be created for free at{' '} + mapbox.com. +

+

+ Make sure the access token is added as the MAPBOX_TOKEN environment + variable. +

+
+ ) + } + + return ( + + + {/* */} + {/* */} + + {mapContainer} + + + ) +} + +const configureMapbox = + ({ + mapboxData, + dispatchMarkerMedia, + }: { + mapboxData?: mediaGeoJson + dispatchMarkerMedia: React.Dispatch + }) => + (map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl) => { + // Add map navigation control + map.addControl(new mapboxLibrary.NavigationControl()) + + map.on('load', () => { + if (map == null) { + console.error('ERROR: map is null') + return + } + + map.addSource('media', { + type: 'geojson', + data: mapboxData?.myMediaGeoJson as never, + cluster: true, + clusterRadius: 50, + clusterProperties: { + thumbnail: ['coalesce', ['get', 'thumbnail'], false], + }, + }) + + // Add dummy layer for features to be queryable + map.addLayer({ + id: 'media-points', + type: 'circle', + source: 'media', + filter: ['!', true], + }) + + registerMediaMarkers({ + map: map, + mapboxLibrary, + dispatchMarkerMedia, + }) + }) + } + +export default MapPage diff --git a/ui/src/Pages/PlacesPage/__generated__/mediaGeoJson.ts b/ui/src/Pages/PlacesPage/__generated__/mediaGeoJson.ts new file mode 100644 index 0000000..b44852c --- /dev/null +++ b/ui/src/Pages/PlacesPage/__generated__/mediaGeoJson.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mediaGeoJson +// ==================================================== + +export interface mediaGeoJson { + /** + * Get media owned by the logged in user, returned in GeoJson format + */ + myMediaGeoJson: Any; +} diff --git a/ui/src/Pages/PlacesPage/__generated__/placePageMapboxToken.ts b/ui/src/Pages/PlacesPage/__generated__/placePageMapboxToken.ts new file mode 100644 index 0000000..a88c85e --- /dev/null +++ b/ui/src/Pages/PlacesPage/__generated__/placePageMapboxToken.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: placePageMapboxToken +// ==================================================== + +export interface placePageMapboxToken { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null + /** + * Get media owned by the logged in user, returned in GeoJson format + */ + myMediaGeoJson: any +} diff --git a/ui/src/Pages/PlacesPage/__generated__/placePageQueryMedia.ts b/ui/src/Pages/PlacesPage/__generated__/placePageQueryMedia.ts new file mode 100644 index 0000000..edfadda --- /dev/null +++ b/ui/src/Pages/PlacesPage/__generated__/placePageQueryMedia.ts @@ -0,0 +1,92 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: placePageQueryMedia +// ==================================================== + +export interface placePageQueryMedia_mediaList_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface placePageQueryMedia_mediaList_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface placePageQueryMedia_mediaList_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface placePageQueryMedia_mediaList { + __typename: "Media"; + id: string; + title: string; + /** + * A short string that can be used to generate a blured version of the media, to show while the original is loading + */ + blurhash: string | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: placePageQueryMedia_mediaList_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: placePageQueryMedia_mediaList_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: placePageQueryMedia_mediaList_videoWeb | null; + type: MediaType; +} + +export interface placePageQueryMedia { + /** + * Get a list of media by their ids, user must own the media or be admin + */ + mediaList: placePageQueryMedia_mediaList[]; +} + +export interface placePageQueryMediaVariables { + mediaIDs: string[]; +} diff --git a/ui/src/Pages/PlacesPage/image-popup.svg b/ui/src/Pages/PlacesPage/image-popup.svg new file mode 100644 index 0000000..da1249e --- /dev/null +++ b/ui/src/Pages/PlacesPage/image-popup.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/Pages/PlacesPage/placesReducer.ts b/ui/src/Pages/PlacesPage/placesReducer.ts new file mode 100644 index 0000000..08be8a2 --- /dev/null +++ b/ui/src/Pages/PlacesPage/placesReducer.ts @@ -0,0 +1,41 @@ +import { PresentMarker } from './PlacesPage' +import { + MediaGalleryState, + PhotoGalleryAction, + mediaGalleryReducer, +} from '../../components/photoGallery/mediaGalleryReducer' + +export interface PlacesState extends MediaGalleryState { + presentMarker?: PresentMarker +} + +export type PlacesAction = + | PhotoGalleryAction + | { type: 'replacePresentMarker'; marker?: PresentMarker } + +export function placesReducer( + state: PlacesState, + action: PlacesAction +): PlacesState { + switch (action.type) { + case 'replacePresentMarker': + if ( + state.presentMarker && + action.marker && + state.presentMarker.cluster === action.marker.cluster && + state.presentMarker.id === action.marker.id + ) { + return { + ...state, + presenting: true, + } + } else { + return { + ...state, + presentMarker: action.marker, + } + } + default: + return mediaGalleryReducer(state, action) + } +} diff --git a/ui/src/Pages/SettingsPage/PeriodicScanner.test.tsx b/ui/src/Pages/SettingsPage/PeriodicScanner.test.tsx new file mode 100644 index 0000000..b36d64c --- /dev/null +++ b/ui/src/Pages/SettingsPage/PeriodicScanner.test.tsx @@ -0,0 +1,67 @@ +import { MockedProvider } from '@apollo/client/testing' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import React from 'react' +import PeriodicScanner, { + SCAN_INTERVAL_MUTATION, + SCAN_INTERVAL_QUERY, +} from './PeriodicScanner' + +test('Enable periodic scanner', async () => { + const graphqlMocks = [ + { + request: { + query: SCAN_INTERVAL_QUERY, + }, + result: { + data: { + siteInfo: { periodicScanInterval: 7380, __typename: 'SiteInfo' }, + }, + }, + }, + { + request: { + query: SCAN_INTERVAL_MUTATION, + variables: { interval: 0 }, + }, + result: { data: { setPeriodicScanInterval: 0 } }, + }, + { + request: { + query: SCAN_INTERVAL_MUTATION, + variables: { interval: 123 * 60 }, + }, + result: { data: { setPeriodicScanInterval: 123 * 60 } }, + }, + ] + + render( + + + + ) + + const enableCheckbox = screen.getByLabelText('Enable periodic scanner') + const inputField = screen.getByLabelText('Interval value') + const unitDropdown = screen.getByLabelText('Interval unit') + + expect(inputField).toBeDisabled() + expect(unitDropdown).toBeDisabled() + + fireEvent.click(enableCheckbox) + + expect(inputField).toBeEnabled() + expect(unitDropdown).toBeEnabled() + + fireEvent.change(unitDropdown, { target: { value: 'minute' } }) + fireEvent.change(inputField, { target: { value: '123' } }) + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument() + }) + + fireEvent.keyUp(inputField, { key: 'Enter' }) + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument() + }) +}) diff --git a/ui/src/Pages/SettingsPage/PeriodicScanner.tsx b/ui/src/Pages/SettingsPage/PeriodicScanner.tsx new file mode 100644 index 0000000..68a1b59 --- /dev/null +++ b/ui/src/Pages/SettingsPage/PeriodicScanner.tsx @@ -0,0 +1,256 @@ +import { gql } from '@apollo/client' +import React, { useRef, useState } from 'react' +import { useMutation, useQuery } from '@apollo/client' +import { InputLabelDescription, InputLabelTitle } from './SettingsPage' +import { useTranslation } from 'react-i18next' +import { scanIntervalQuery } from './__generated__/scanIntervalQuery' +import { + changeScanIntervalMutation, + changeScanIntervalMutationVariables, +} from './__generated__/changeScanIntervalMutation' +import Checkbox from '../../primitives/form/Checkbox' +import { TextField } from '../../primitives/form/Input' +import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown' +import Loader from '../../primitives/Loader' + +export const SCAN_INTERVAL_QUERY = gql` + query scanIntervalQuery { + siteInfo { + periodicScanInterval + } + } +` + +export const SCAN_INTERVAL_MUTATION = gql` + mutation changeScanIntervalMutation($interval: Int!) { + setPeriodicScanInterval(interval: $interval) + } +` + +enum TimeUnit { + Second = 'second', + Minute = 'minute', + Hour = 'hour', + Day = 'day', + Month = 'month', +} + +const timeUnits = [ + { + value: TimeUnit.Second, + multiplier: 1, + }, + { + value: TimeUnit.Minute, + multiplier: 60, + }, + { + value: TimeUnit.Hour, + multiplier: 60 * 60, + }, + { + value: TimeUnit.Day, + multiplier: 60 * 60 * 24, + }, + { + value: TimeUnit.Month, + multiplier: 60 * 60 * 24 * 30, + }, +] + +type TimeValue = { + value: number + unit: TimeUnit +} + +const convertToSeconds = ({ value, unit }: TimeValue) => { + return value * (timeUnits.find(x => x.value == unit)?.multiplier as number) +} + +const convertToAppropriateUnit = ({ value, unit }: TimeValue): TimeValue => { + if (value == 0) { + return { + unit: TimeUnit.Second, + value: 0, + } + } + + const seconds = convertToSeconds({ value, unit }) + + let resultingUnit = timeUnits[0] + for (const unit of timeUnits) { + if ( + seconds / unit.multiplier >= 1 && + (seconds / unit.multiplier) % 1 == 0 + ) { + resultingUnit = unit + } else { + break + } + } + + return { + unit: resultingUnit.value, + value: seconds / resultingUnit.multiplier, + } +} + +const PeriodicScanner = () => { + const { t } = useTranslation() + + const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false) + const [scanInterval, setScanInterval] = useState({ + value: 0, + unit: TimeUnit.Second, + }) + + const scanIntervalServerValue = useRef(null) + + const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, { + onCompleted(data) { + const queryScanInterval = data.siteInfo.periodicScanInterval + + if (queryScanInterval == 0) { + setScanInterval({ + unit: TimeUnit.Second, + value: 0, + }) + } else { + setScanInterval( + convertToAppropriateUnit({ + unit: TimeUnit.Second, + value: queryScanInterval, + }) + ) + } + + setEnablePeriodicScanner(queryScanInterval > 0) + }, + }) + + const [setScanIntervalMutation, { loading: scanIntervalMutationLoading }] = + useMutation< + changeScanIntervalMutation, + changeScanIntervalMutationVariables + >(SCAN_INTERVAL_MUTATION) + + const onScanIntervalCheckboxChange = (checked: boolean) => { + setEnablePeriodicScanner(checked) + + onScanIntervalUpdate( + checked ? scanInterval : { value: 0, unit: TimeUnit.Second } + ) + } + + const onScanIntervalUpdate = (scanInterval: TimeValue) => { + const seconds = convertToSeconds(scanInterval) + + if (scanIntervalServerValue.current != seconds) { + setScanIntervalMutation({ + variables: { + interval: seconds, + }, + }) + scanIntervalServerValue.current = seconds + } + } + + const scanIntervalUnits: DropdownItem[] = [ + { + label: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'), + value: TimeUnit.Second, + }, + { + label: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'), + value: TimeUnit.Minute, + }, + { + label: t('settings.periodic_scanner.interval_unit.hour', 'Hour'), + value: TimeUnit.Hour, + }, + { + label: t('settings.periodic_scanner.interval_unit.days', 'Days'), + value: TimeUnit.Day, + }, + { + label: t('settings.periodic_scanner.interval_unit.months', 'Months'), + value: TimeUnit.Month, + }, + ] + + return ( + <> +

+ {t('settings.periodic_scanner.title', 'Periodic scanner')} +

+ + + onScanIntervalCheckboxChange(event.target.checked || false) + } + /> + +
+ +
+ { + setScanInterval(x => ({ + value: Number(e.target.value), + unit: x.unit, + })) + }} + action={() => { + onScanIntervalUpdate(scanInterval) + }} + /> + { + const newScanInterval: TimeValue = { + ...scanInterval, + unit: value as TimeUnit, + } + + setScanInterval(newScanInterval) + onScanIntervalUpdate(newScanInterval) + }} + /> +
+
+ + + ) +} + +export default PeriodicScanner diff --git a/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.test.tsx b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.test.tsx new file mode 100644 index 0000000..42b42da --- /dev/null +++ b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.test.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import { MockedProvider } from '@apollo/client/testing' + +import { render, screen } from '@testing-library/react' + +import { + CONCURRENT_WORKERS_QUERY, + SET_CONCURRENT_WORKERS_MUTATION, + ScannerConcurrentWorkers, +} from './ScannerConcurrentWorkers' + +test('load ScannerConcurrentWorkers', () => { + const graphqlMocks = [ + { + request: { + query: CONCURRENT_WORKERS_QUERY, + }, + result: { + data: { + siteInfo: { concurrentWorkers: 3 }, + }, + }, + }, + { + request: { + query: SET_CONCURRENT_WORKERS_MUTATION, + variables: { + workers: '1', + }, + }, + result: { + data: {}, + }, + }, + ] + render( + + + + ) + + expect(screen.getByText('Scanner concurrent workers')).toBeInTheDocument() +}) diff --git a/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx new file mode 100644 index 0000000..8a10dcb --- /dev/null +++ b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx @@ -0,0 +1,88 @@ +import React, { useRef, useState } from 'react' +import { useQuery, useMutation, gql } from '@apollo/client' +import { InputLabelTitle, InputLabelDescription } from './SettingsPage' +import { useTranslation } from 'react-i18next' +import { concurrentWorkersQuery } from './__generated__/concurrentWorkersQuery' +import { + setConcurrentWorkers, + setConcurrentWorkersVariables, +} from './__generated__/setConcurrentWorkers' +import { TextField } from '../../primitives/form/Input' + +export const CONCURRENT_WORKERS_QUERY = gql` + query concurrentWorkersQuery { + siteInfo { + concurrentWorkers + } + } +` + +export const SET_CONCURRENT_WORKERS_MUTATION = gql` + mutation setConcurrentWorkers($workers: Int!) { + setScannerConcurrentWorkers(workers: $workers) + } +` + +export const ScannerConcurrentWorkers = () => { + const { t } = useTranslation() + + const workerAmountServerValue = useRef(null) + const [workerAmount, setWorkerAmount] = useState(0) + + const workerAmountQuery = useQuery( + CONCURRENT_WORKERS_QUERY, + { + onCompleted(data) { + setWorkerAmount(data.siteInfo.concurrentWorkers) + workerAmountServerValue.current = data.siteInfo.concurrentWorkers + }, + } + ) + + const [setWorkersMutation, workersMutationData] = useMutation< + setConcurrentWorkers, + setConcurrentWorkersVariables + >(SET_CONCURRENT_WORKERS_MUTATION) + + const updateWorkerAmount = (workerAmount: number) => { + if (workerAmountServerValue.current != workerAmount) { + workerAmountServerValue.current = workerAmount + setWorkersMutation({ + variables: { + workers: workerAmount, + }, + }) + } + } + + return ( +
+ + { + setWorkerAmount(parseInt(event.target.value)) + }} + onBlur={() => updateWorkerAmount(workerAmount)} + onKeyDown={event => + event.key == 'Enter' && updateWorkerAmount(workerAmount) + } + /> +
+ ) +} diff --git a/ui/src/Pages/SettingsPage/ScannerSection.tsx b/ui/src/Pages/SettingsPage/ScannerSection.tsx new file mode 100644 index 0000000..733b007 --- /dev/null +++ b/ui/src/Pages/SettingsPage/ScannerSection.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { useMutation, gql } from '@apollo/client' +import PeriodicScanner from './PeriodicScanner' +import { ScannerConcurrentWorkers } from './ScannerConcurrentWorkers' +import { SectionTitle, InputLabelDescription } from './SettingsPage' +import { useTranslation } from 'react-i18next' +import { scanAllMutation } from './__generated__/scanAllMutation' +import { Button } from '../../primitives/form/Input' + +const SCAN_MUTATION = gql` + mutation scanAllMutation { + scanAll { + success + message + } + } +` + +const ScannerSection = () => { + const { t } = useTranslation() + const [startScanner, { called }] = useMutation(SCAN_MUTATION) + + return ( +
+ + {t('settings.scanner.title', 'Scanner')} + + + {t( + 'settings.scanner.description', + 'Will scan all users for new or updated media' + )} + + + + +
+ ) +} + +export default ScannerSection diff --git a/ui/src/Pages/SettingsPage/SettingsPage.tsx b/ui/src/Pages/SettingsPage/SettingsPage.tsx new file mode 100644 index 0000000..9da1757 --- /dev/null +++ b/ui/src/Pages/SettingsPage/SettingsPage.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { useIsAdmin } from '../../components/routes/AuthorizedRoute' +import Layout from '../../components/layout/Layout' +import ScannerSection from './ScannerSection' +import UserPreferences from './UserPreferences' +import UsersTable from './Users/UsersTable' +import VersionInfo from './VersionInfo' +import classNames from 'classnames' + +type SectionTitleProps = { + children: string + nospace?: boolean +} + +export const SectionTitle = ({ children, nospace }: SectionTitleProps) => { + return ( +

+ {children} +

+ ) +} + +export const InputLabelTitle = styled.h3.attrs({ + className: 'font-semibold mt-4', +})`` + +export const InputLabelDescription = styled.p.attrs({ + className: 'text-sm mb-2', +})`` + +const SettingsPage = () => { + const { t } = useTranslation() + const isAdmin = useIsAdmin() + + return ( + + + {isAdmin && ( + <> + + + + )} + + + ) +} + +export default SettingsPage diff --git a/ui/src/Pages/SettingsPage/UserPreferences.tsx b/ui/src/Pages/SettingsPage/UserPreferences.tsx new file mode 100644 index 0000000..9dba134 --- /dev/null +++ b/ui/src/Pages/SettingsPage/UserPreferences.tsx @@ -0,0 +1,181 @@ +import { useMutation, useQuery } from '@apollo/client' +import gql from 'graphql-tag' +import React, { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { LanguageTranslation } from '../../__generated__/globalTypes' +import Dropdown from '../../primitives/form/Dropdown' +import { Button } from '../../primitives/form/Input' +import { + InputLabelDescription, + InputLabelTitle, + SectionTitle, +} from './SettingsPage' +import { + changeUserPreferences, + changeUserPreferencesVariables, +} from './__generated__/changeUserPreferences' +import { myUserPreferences } from './__generated__/myUserPreferences' +import { TranslationFn } from '../../localization' +import { changeTheme, getTheme } from '../../theme' + +const languagePreferences = [ + { key: 1, label: 'English', value: LanguageTranslation.English }, + { key: 2, label: 'Français', value: LanguageTranslation.French }, + { key: 3, label: 'Svenska', value: LanguageTranslation.Swedish }, + { key: 4, label: 'Dansk', value: LanguageTranslation.Danish }, + { key: 5, label: 'Español', value: LanguageTranslation.Spanish }, + { key: 6, label: 'Polski', value: LanguageTranslation.Polish }, + { key: 7, label: 'Italiano', value: LanguageTranslation.Italian }, + { key: 8, label: 'Deutsch', value: LanguageTranslation.German }, + { key: 9, label: 'Русский', value: LanguageTranslation.Russian }, + { key: 10, label: '繁體中文 (香港)', value: LanguageTranslation.TraditionalChineseHK }, + { key: 16, label: '繁體中文 (台灣)', value: LanguageTranslation.TraditionalChineseTW }, + { key: 11, label: '简体中文', value: LanguageTranslation.SimplifiedChinese }, + { key: 12, label: 'Português', value: LanguageTranslation.Portuguese }, + { key: 13, label: 'Euskara', value: LanguageTranslation.Basque }, + { key: 14, label: 'Türkçe', value: LanguageTranslation.Turkish }, + { key: 15, label: 'Українська', value: LanguageTranslation.Ukrainian }, + { key: 17, label: '日本語', value: LanguageTranslation.Japanese }, + { key: 18, label: 'Nederlands', value: LanguageTranslation.Dutch }, +] + +const themePreferences = (t: TranslationFn) => [ + { + key: 1, + label: t('settings.user_preferences.theme.auto.label', 'Same as system'), + value: 'auto', + }, + { + key: 2, + label: t('settings.user_preferences.theme.light.label', 'Light'), + value: 'light', + }, + { + key: 2, + label: t('settings.user_preferences.theme.dark.label', 'Dark'), + value: 'dark', + }, +] + +const CHANGE_USER_PREFERENCES = gql` + mutation changeUserPreferences($language: String) { + changeUserPreferences(language: $language) { + id + language + } + } +` + +const MY_USER_PREFERENCES = gql` + query myUserPreferences { + myUserPreferences { + id + language + } + } +` + +const LogoutButton = () => { + const { t } = useTranslation() + + return ( + + ) +} + +const UserPreferencesWrapper = styled.div` + margin-bottom: 24px; +` + +const UserPreferences = () => { + const { t } = useTranslation() + const [theme, setTheme] = useState(getTheme()) + + const changeStateTheme = (value: string) => { + changeTheme(value) + setTheme(value) + } + + const { data } = useQuery(MY_USER_PREFERENCES) + + const [changePrefs, { loading: loadingPrefs, error }] = useMutation< + changeUserPreferences, + changeUserPreferencesVariables + >(CHANGE_USER_PREFERENCES) + + const sortedLanguagePrefs = useMemo( + () => [...languagePreferences].sort((a, b) => a.label.localeCompare(b.label)), + [] + ) + + if (error) { + return
{error.message}
+ } + + return ( + + + {t('settings.user_preferences.title', 'User preferences')} + + + + { + changePrefs({ + variables: { + language: language as LanguageTranslation, + }, + }) + }} + selected={data?.myUserPreferences.language || undefined} + disabled={loadingPrefs} + /> + + + + ) +} + +export default UserPreferences diff --git a/ui/src/Pages/SettingsPage/Users/AddUserRow.test.tsx b/ui/src/Pages/SettingsPage/Users/AddUserRow.test.tsx new file mode 100644 index 0000000..0ffabb0 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/AddUserRow.test.tsx @@ -0,0 +1,93 @@ +import React from 'react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import AddUserRow, { + CREATE_USER_MUTATION, + USER_ADD_ROOT_PATH_MUTATION, +} from './AddUserRow' +import { MockedProvider } from '@apollo/client/testing' + +const gqlMock = [ + { + request: { + query: CREATE_USER_MUTATION, + variables: { username: 'testuser', admin: false }, + }, + result: { + data: { + createUser: { + id: '123', + username: 'testuser', + admin: false, + __typename: 'User', + }, + }, + }, + }, + { + request: { + query: USER_ADD_ROOT_PATH_MUTATION, + variables: { id: '123', rootPath: '/tmp' }, + }, + result: { data: { userAddRootPath: { id: '567', __typename: 'Album' } } }, + }, +] + +test('Add user with username and path', async () => { + const userAdded = vi.fn() + const setShow = vi.fn() + + render( + + + + + +
+
+ ) + + const usernameInput = screen.getByPlaceholderText('Username') + const pathInput = screen.getByPlaceholderText('/path/to/photos') + const addUserBtn = screen.getByText('Add user') + + fireEvent.change(usernameInput, { target: { value: 'testuser' } }) + fireEvent.change(pathInput, { target: { value: '/tmp' } }) + fireEvent.click(addUserBtn) + + await waitFor(() => { + expect(userAdded).toHaveBeenCalledTimes(1) + }) + + expect(setShow).not.toHaveBeenCalled() +}) + +test('Add user with only username', async () => { + const userAdded = vi.fn() + const setShow = vi.fn() + + render( + + + + + +
+
+ ) + + const usernameInput = screen.getByPlaceholderText('Username') + const addUserBtn = screen.getByText('Add user') + + // don't set path + // const pathInput = screen.getByPlaceholderText('/path/to/photos') + // fireEvent.change(pathInput, { target: { value: '/tmp' } }) + + fireEvent.change(usernameInput, { target: { value: 'testuser' } }) + fireEvent.click(addUserBtn) + + await waitFor(() => { + expect(userAdded).toHaveBeenCalledTimes(1) + }) + + expect(setShow).not.toHaveBeenCalled() +}) diff --git a/ui/src/Pages/SettingsPage/Users/AddUserRow.tsx b/ui/src/Pages/SettingsPage/Users/AddUserRow.tsx new file mode 100644 index 0000000..ea35f1e --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/AddUserRow.tsx @@ -0,0 +1,157 @@ +import { gql, useMutation } from '@apollo/client' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import Checkbox from '../../../primitives/form/Checkbox' +import { TextField, Button, ButtonGroup } from '../../../primitives/form/Input' +import { TableRow, TableCell } from '../../../primitives/Table' +import { createUser, createUserVariables } from './__generated__/createUser' +import { + userAddRootPath, + userAddRootPathVariables, +} from './__generated__/userAddRootPath' + +export const CREATE_USER_MUTATION = gql` + mutation createUser($username: String!, $admin: Boolean!) { + createUser(username: $username, admin: $admin) { + id + username + admin + __typename + } + } +` + +export const USER_ADD_ROOT_PATH_MUTATION = gql` + mutation userAddRootPath($id: ID!, $rootPath: String!) { + userAddRootPath(id: $id, rootPath: $rootPath) { + id + } + } +` + +const initialState = { + username: '', + rootPath: '', + admin: false, + userAdded: false, +} + +type AddUserRowProps = { + setShow: React.Dispatch> + show: boolean + onUserAdded(): void +} + +const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => { + const { t } = useTranslation() + const [state, setState] = useState(initialState) + + const finished = () => { + setState(initialState) + onUserAdded() + } + + const [addRootPath, { loading: addRootPathLoading }] = useMutation< + userAddRootPath, + userAddRootPathVariables + >(USER_ADD_ROOT_PATH_MUTATION, { + onCompleted: () => { + finished() + }, + onError: () => { + finished() + }, + }) + + const [createUser, { loading: createUserLoading }] = useMutation< + createUser, + createUserVariables + >(CREATE_USER_MUTATION, { + onCompleted: ({ createUser: { id } }) => { + if (state.rootPath) { + addRootPath({ + variables: { + id: id, + rootPath: state.rootPath, + }, + }) + } else { + finished() + } + }, + }) + + const loading = addRootPathLoading || createUserLoading + + function updateInput( + event: React.ChangeEvent, + key: string + ) { + setState({ + ...state, + [key]: event.target.value, + }) + } + + if (!show) { + return null + } + + return ( + + + updateInput(e, 'username')} + /> + + + updateInput(e, 'rootPath')} + /> + + + { + setState({ + ...state, + admin: e.target.checked || false, + }) + }} + /> + + + + + + + + + ) +} + +export default AddUserRow diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRow.tsx b/ui/src/Pages/SettingsPage/Users/EditUserRow.tsx new file mode 100644 index 0000000..5340924 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/EditUserRow.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { EditRootPaths } from './EditUserRowRootPaths' +import { UserRowChildProps } from './UserRow' +import { TableRow, TableCell } from '../../../primitives/Table' +import { TextField } from '../../../primitives/form/Input' +import { Button, ButtonGroup } from '../../../primitives/form/Input' +import Checkbox from '../../../primitives/form/Checkbox' + +const EditUserRow = ({ + user, + state, + setState, + updateUser, + updateUserLoading, +}: UserRowChildProps) => { + const { t } = useTranslation() + + function updateInput( + event: React.ChangeEvent, + key: string + ) { + setState(state => ({ + ...state, + [key]: event.target.value, + })) + } + + return ( + + + updateInput(e, 'username')} + /> + + + + + + { + setState(state => ({ + ...state, + admin: e.target.checked || false, + })) + }} + /> + + + + + + + + + ) +} + +export default EditUserRow diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx new file mode 100644 index 0000000..3b68d43 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react' +import { gql, useMutation } from '@apollo/client' +import { USERS_QUERY } from './UsersTable' +import { useTranslation } from 'react-i18next' +import { USER_ADD_ROOT_PATH_MUTATION } from './AddUserRow' +import { + userRemoveAlbumPathMutation, + userRemoveAlbumPathMutationVariables, +} from './__generated__/userRemoveAlbumPathMutation' +import { + settingsUsersQuery_user, + settingsUsersQuery_user_rootAlbums, +} from './__generated__/settingsUsersQuery' +import { userAddRootPath } from './__generated__/userAddRootPath' +import { Button, TextField } from '../../../primitives/form/Input' + +const USER_REMOVE_ALBUM_PATH_MUTATION = gql` + mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) { + userRemoveRootAlbum(userId: $userId, albumId: $albumId) { + id + } + } +` + +type EditRootPathProps = { + album: settingsUsersQuery_user_rootAlbums + user: settingsUsersQuery_user +} + +const EditRootPath = ({ album, user }: EditRootPathProps) => { + const { t } = useTranslation() + const [removeAlbumPath, { loading }] = useMutation< + userRemoveAlbumPathMutation, + userRemoveAlbumPathMutationVariables + >(USER_REMOVE_ALBUM_PATH_MUTATION, { + refetchQueries: [ + { + query: USERS_QUERY, + }, + ], + }) + + return ( +
  • + {album.filePath} + +
  • + ) +} + +type EditNewRootPathProps = { + userID: string +} + +const EditNewRootPath = ({ userID }: EditNewRootPathProps) => { + const { t } = useTranslation() + const [value, setValue] = useState('') + const [addRootPath, { loading }] = useMutation( + USER_ADD_ROOT_PATH_MUTATION, + { + refetchQueries: [ + { + query: USERS_QUERY, + }, + ], + } + ) + + return ( +
  • + ) => + setValue(e.target.value) + } + disabled={loading} + /> + +
  • + ) +} + +type EditRootPathsProps = { + user: settingsUsersQuery_user +} + +export const EditRootPaths = ({ user }: EditRootPathsProps) => { + const editRows = user.rootAlbums.map(album => ( + + )) + + return ( +
      + {editRows} + +
    + ) +} diff --git a/ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx b/ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx new file mode 100644 index 0000000..76f18da --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react' +import { gql, useMutation } from '@apollo/client' +import { Trans, useTranslation } from 'react-i18next' +import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery' +import Modal from '../../../primitives/Modal' +import { TextField } from '../../../primitives/form/Input' + +const changeUserPasswordMutation = gql` + mutation changeUserPassword($userId: ID!, $password: String!) { + updateUser(id: $userId, password: $password) { + id + } + } +` + +interface ChangePasswordModalProps { + onClose(): void + open: boolean + user: settingsUsersQuery_user +} + +const ChangePasswordModal = ({ + onClose, + user, + open, +}: ChangePasswordModalProps) => { + const { t } = useTranslation() + const [passwordInput, setPasswordInput] = useState('') + + const [changePassword] = useMutation(changeUserPasswordMutation, { + onCompleted: () => { + onClose && onClose() + }, + }) + + return ( + + Change password for {user.username} + + } + actions={[ + { + key: 'cancel', + label: t('general.action.cancel', 'Cancel'), + onClick: () => onClose && onClose(), + }, + { + key: 'change_password', + label: t( + 'settings.users.password_reset.form.submit', + 'Change password' + ), + variant: 'positive', + onClick: () => { + changePassword({ + variables: { + userId: user.id, + password: passwordInput, + }, + }) + }, + }, + ]} + > +
    + setPasswordInput(e.target.value)} + type="password" + /> +
    +
    + ) +} + +export default ChangePasswordModal diff --git a/ui/src/Pages/SettingsPage/Users/UserRow.tsx b/ui/src/Pages/SettingsPage/Users/UserRow.tsx new file mode 100644 index 0000000..9f0573a --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/UserRow.tsx @@ -0,0 +1,137 @@ +import React, { useState } from 'react' +import { + FetchResult, + gql, + MutationFunctionOptions, + useMutation, +} from '@apollo/client' +import EditUserRow from './EditUserRow' +import ViewUserRow from './ViewUserRow' +import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery' +import { scanUser, scanUserVariables } from './__generated__/scanUser' +import { updateUser, updateUserVariables } from './__generated__/updateUser' +import { deleteUser, deleteUserVariables } from './__generated__/deleteUser' + +const updateUserMutation = gql` + mutation updateUser($id: ID!, $username: String, $admin: Boolean) { + updateUser(id: $id, username: $username, admin: $admin) { + id + username + admin + } + } +` + +const deleteUserMutation = gql` + mutation deleteUser($id: ID!) { + deleteUser(id: $id) { + id + username + } + } +` + +const scanUserMutation = gql` + mutation scanUser($userId: ID!) { + scanUser(userId: $userId) { + success + } + } +` + +interface UserRowState extends settingsUsersQuery_user { + editing: boolean + newRootPath: string + oldState?: Omit +} + +type ApolloMutationFn = ( + options?: MutationFunctionOptions + // eslint-disable-next-line @typescript-eslint/no-explicit-any +) => Promise> + +export type UserRowChildProps = { + user: settingsUsersQuery_user + state: UserRowState + setState: React.Dispatch> + scanUser: ApolloMutationFn + updateUser: ApolloMutationFn + updateUserLoading: boolean + deleteUser: ApolloMutationFn + setChangePassword: React.Dispatch> + setConfirmDelete: React.Dispatch> + scanUserCalled: boolean + showChangePassword: boolean + showConfirmDelete: boolean +} + +export type UserRowProps = { + user: settingsUsersQuery_user + refetchUsers: () => void +} + +const UserRow = ({ user, refetchUsers }: UserRowProps) => { + const [state, setState] = useState({ + ...user, + editing: false, + newRootPath: '', + }) + + const [showConfirmDelete, setConfirmDelete] = useState(false) + const [showChangePassword, setChangePassword] = useState(false) + + const [updateUser, { loading: updateUserLoading }] = useMutation< + updateUser, + updateUserVariables + >(updateUserMutation, { + onCompleted: data => { + setState(state => ({ + ...state, + ...data.updateUser, + editing: false, + })) + refetchUsers() + }, + }) + + const [deleteUser] = useMutation( + deleteUserMutation, + { + onCompleted: () => { + refetchUsers() + }, + } + ) + + const [scanUser, { called: scanUserCalled }] = useMutation< + scanUser, + scanUserVariables + >(scanUserMutation, { + onCompleted: () => { + refetchUsers() + }, + }) + + const props: UserRowChildProps = { + user, + state, + setState, + scanUser, + updateUser, + updateUserLoading, + deleteUser, + setChangePassword, + setConfirmDelete, + scanUserCalled, + showChangePassword, + showConfirmDelete, + } + + if (state.editing) { + return + } + + return +} + +export default UserRow diff --git a/ui/src/Pages/SettingsPage/Users/UsersTable.tsx b/ui/src/Pages/SettingsPage/Users/UsersTable.tsx new file mode 100644 index 0000000..2ff38dd --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/UsersTable.tsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react' +import { + Table, + TableHeader, + TableHeaderCell, + TableRow, + TableBody, + TableFooter, + TableScrollWrapper, +} from '../../../primitives/Table' +import { useQuery, gql } from '@apollo/client' +import UserRow from './UserRow' +import AddUserRow from './AddUserRow' +import { SectionTitle } from '../SettingsPage' +import { useTranslation } from 'react-i18next' +import { settingsUsersQuery } from './__generated__/settingsUsersQuery' +import { Button } from '../../../primitives/form/Input' +import Loader from '../../../primitives/Loader' + +export const USERS_QUERY = gql` + query settingsUsersQuery { + user { + id + username + admin + rootAlbums { + id + filePath + } + } + } +` + +const UsersTable = () => { + const { t } = useTranslation() + const [showAddUser, setShowAddUser] = useState(false) + + const { loading, error, data, refetch } = + useQuery(USERS_QUERY) + + if (error) { + return
    {`Users table error: ${error.message}`}
    + } + + let userRows: JSX.Element[] = [] + if (data?.user) { + userRows = data.user.map(user => ( + + )) + } + + return ( +
    + {t('settings.users.title', 'Users')} + + + + + + + {t('settings.users.table.column_names.username', 'Username')} + + + {t( + 'settings.users.table.column_names.photo_path', + 'Photo path' + )} + + + {t( + 'settings.users.table.column_names.capabilities', + 'Capabilities' + )} + + + {t('settings.users.table.column_names.action', 'Action')} + + + + + + {userRows} + { + setShowAddUser(false) + refetch() + }} + /> + + + + + + + + + +
    +
    +
    + ) +} + +export default UsersTable diff --git a/ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx b/ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx new file mode 100644 index 0000000..bad5a3b --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx @@ -0,0 +1,123 @@ +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import Checkbox from '../../../primitives/form/Checkbox' +import { Button } from '../../../primitives/form/Input' +import Modal from '../../../primitives/Modal' +import { TableCell, TableRow } from '../../../primitives/Table' +import ChangePasswordModal from './UserChangePassword' +import { UserRowChildProps } from './UserRow' + +const ViewUserRow = ({ + user, + // state, + setState, + scanUser, + deleteUser, + setChangePassword, + setConfirmDelete, + scanUserCalled, + showChangePassword, + showConfirmDelete, +}: UserRowChildProps) => { + const { t } = useTranslation() + const paths = ( +
      + {user.rootAlbums.map(album => ( +
    • {album.filePath}
    • + ))} +
    + ) + + return ( + + {user.username} + {paths} + + + + +
    + + + + setChangePassword(false)} + /> + + setConfirmDelete(false)} + title={t('settings.users.confirm_delete_user.title', 'Delete user')} + actions={[ + { + key: 'cancel', + label: t('general.action.cancel', 'Cancel'), + onClick: () => setConfirmDelete(false), + }, + { + key: 'delete', + label: t( + 'settings.users.confirm_delete_user.action', + 'Delete {{user}}', + { user: user.username } + ), + onClick: () => { + setConfirmDelete(false) + deleteUser({ + variables: { + id: user.id, + }, + }) + }, + variant: 'negative', + }, + ]} + description={ + +

    + {`Are you sure, you want to delete `} + {user.username}? +

    +

    {`This action cannot be undone`}

    +
    + } + /> +
    +
    +
    + ) +} + +export default ViewUserRow diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/changeUserPassword.ts b/ui/src/Pages/SettingsPage/Users/__generated__/changeUserPassword.ts new file mode 100644 index 0000000..b30d352 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/changeUserPassword.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: changeUserPassword +// ==================================================== + +export interface changeUserPassword_updateUser { + __typename: "User"; + id: string; +} + +export interface changeUserPassword { + /** + * Update a user, fields left as `null` will not be changed + */ + updateUser: changeUserPassword_updateUser; +} + +export interface changeUserPasswordVariables { + userId: string; + password: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/createUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/createUser.ts new file mode 100644 index 0000000..393ad57 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/createUser.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: createUser +// ==================================================== + +export interface createUser_createUser { + __typename: "User"; + id: string; + username: string; + /** + * Whether or not the user has admin privileges + */ + admin: boolean; +} + +export interface createUser { + /** + * Create a new user + */ + createUser: createUser_createUser; +} + +export interface createUserVariables { + username: string; + admin: boolean; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/deleteUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/deleteUser.ts new file mode 100644 index 0000000..ddd7bb2 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/deleteUser.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: deleteUser +// ==================================================== + +export interface deleteUser_deleteUser { + __typename: "User"; + id: string; + username: string; +} + +export interface deleteUser { + /** + * Delete an existing user + */ + deleteUser: deleteUser_deleteUser; +} + +export interface deleteUserVariables { + id: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/scanUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/scanUser.ts new file mode 100644 index 0000000..37f0c92 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/scanUser.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: scanUser +// ==================================================== + +export interface scanUser_scanUser { + __typename: "ScannerResult"; + success: boolean; +} + +export interface scanUser { + /** + * Scan a single user for new media + */ + scanUser: scanUser_scanUser; +} + +export interface scanUserVariables { + userId: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/settingsUsersQuery.ts b/ui/src/Pages/SettingsPage/Users/__generated__/settingsUsersQuery.ts new file mode 100644 index 0000000..4270397 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/settingsUsersQuery.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: settingsUsersQuery +// ==================================================== + +export interface settingsUsersQuery_user_rootAlbums { + __typename: "Album"; + id: string; + /** + * The path on the filesystem of the server, where this album is located + */ + filePath: string; +} + +export interface settingsUsersQuery_user { + __typename: "User"; + id: string; + username: string; + /** + * Whether or not the user has admin privileges + */ + admin: boolean; + /** + * Top level albums owned by this user + */ + rootAlbums: settingsUsersQuery_user_rootAlbums[]; +} + +export interface settingsUsersQuery { + /** + * List of registered users, must be admin to call + */ + user: settingsUsersQuery_user[]; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/updateUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/updateUser.ts new file mode 100644 index 0000000..3f5cada --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/updateUser.ts @@ -0,0 +1,31 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: updateUser +// ==================================================== + +export interface updateUser_updateUser { + __typename: "User"; + id: string; + username: string; + /** + * Whether or not the user has admin privileges + */ + admin: boolean; +} + +export interface updateUser { + /** + * Update a user, fields left as `null` will not be changed + */ + updateUser: updateUser_updateUser; +} + +export interface updateUserVariables { + id: string; + username?: string | null; + admin?: boolean | null; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/userAddRootPath.ts b/ui/src/Pages/SettingsPage/Users/__generated__/userAddRootPath.ts new file mode 100644 index 0000000..af58866 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/userAddRootPath.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: userAddRootPath +// ==================================================== + +export interface userAddRootPath_userAddRootPath { + __typename: "Album"; + id: string; +} + +export interface userAddRootPath { + /** + * Add a root path from where to look for media for the given user, specified by their user id. + */ + userAddRootPath: userAddRootPath_userAddRootPath | null; +} + +export interface userAddRootPathVariables { + id: string; + rootPath: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts b/ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts new file mode 100644 index 0000000..3323d5c --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: userRemoveAlbumPathMutation +// ==================================================== + +export interface userRemoveAlbumPathMutation_userRemoveRootAlbum { + __typename: "Album"; + id: string; +} + +export interface userRemoveAlbumPathMutation { + /** + * Remove a root path from a user, specified by the id of the user and the top album representing the root path. + * This album was returned when creating the path using `userAddRootPath`. + * A list of root paths for a particular user can be retrived from the `User.rootAlbums` path. + */ + userRemoveRootAlbum: userRemoveAlbumPathMutation_userRemoveRootAlbum | null; +} + +export interface userRemoveAlbumPathMutationVariables { + userId: string; + albumId: string; +} diff --git a/ui/src/Pages/SettingsPage/VersionInfo.tsx b/ui/src/Pages/SettingsPage/VersionInfo.tsx new file mode 100644 index 0000000..b673ca1 --- /dev/null +++ b/ui/src/Pages/SettingsPage/VersionInfo.tsx @@ -0,0 +1,53 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' +import { useTranslation } from 'react-i18next' +import { + InputLabelDescription, + InputLabelTitle, + SectionTitle, +} from './SettingsPage' + +const VERSION = import.meta.env.REACT_APP_BUILD_VERSION ?? 'undefined' +const BUILD_DATE = import.meta.env.REACT_APP_BUILD_DATE ?? 'undefined' + +const COMMIT_SHA = import.meta.env.REACT_APP_BUILD_COMMIT_SHA +let commitLink: ReactElement + +if (COMMIT_SHA) { + commitLink = React.createElement( + 'a', + { + href: 'https://github.com/photoview/photoview/commit/' + COMMIT_SHA, + title: COMMIT_SHA, + }, + COMMIT_SHA.substring(0, 8) + ) +} + +const VersionInfoWrapper = styled.div` + margin-bottom: 24px; +` + +const VersionInfo = () => { + const { t } = useTranslation() + + return ( + + + {t('settings.version_info.title', 'Photoview Version')} + + + {t('settings.version_info.version_title', 'Release Version')} + + + {VERSION} ({commitLink}) + + + {t('settings.version_info.build_date_title', 'Build date')} + + {BUILD_DATE} + + ) +} + +export default VersionInfo diff --git a/ui/src/Pages/SettingsPage/__generated__/changeScanIntervalMutation.ts b/ui/src/Pages/SettingsPage/__generated__/changeScanIntervalMutation.ts new file mode 100644 index 0000000..f1ff320 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/changeScanIntervalMutation.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: changeScanIntervalMutation +// ==================================================== + +export interface changeScanIntervalMutation { + /** + * Set how often, in seconds, the server should automatically scan for new media, + * a value of 0 will disable periodic scans + */ + setPeriodicScanInterval: number; +} + +export interface changeScanIntervalMutationVariables { + interval: number; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/changeUserPreferences.ts b/ui/src/Pages/SettingsPage/__generated__/changeUserPreferences.ts new file mode 100644 index 0000000..775e6ea --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/changeUserPreferences.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { LanguageTranslation } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: changeUserPreferences +// ==================================================== + +export interface changeUserPreferences_changeUserPreferences { + __typename: "UserPreferences"; + id: string; + language: LanguageTranslation | null; +} + +export interface changeUserPreferences { + /** + * Change user preferences for the logged in user + */ + changeUserPreferences: changeUserPreferences_changeUserPreferences; +} + +export interface changeUserPreferencesVariables { + language?: string | null; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/concurrentWorkersQuery.ts b/ui/src/Pages/SettingsPage/__generated__/concurrentWorkersQuery.ts new file mode 100644 index 0000000..b1bfe3e --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/concurrentWorkersQuery.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: concurrentWorkersQuery +// ==================================================== + +export interface concurrentWorkersQuery_siteInfo { + __typename: "SiteInfo"; + /** + * How many max concurrent scanner jobs that should run at once + */ + concurrentWorkers: number; +} + +export interface concurrentWorkersQuery { + siteInfo: concurrentWorkersQuery_siteInfo; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/myUserPreferences.ts b/ui/src/Pages/SettingsPage/__generated__/myUserPreferences.ts new file mode 100644 index 0000000..4a0014e --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/myUserPreferences.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { LanguageTranslation } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: myUserPreferences +// ==================================================== + +export interface myUserPreferences_myUserPreferences { + __typename: "UserPreferences"; + id: string; + language: LanguageTranslation | null; +} + +export interface myUserPreferences { + /** + * User preferences for the logged in user + */ + myUserPreferences: myUserPreferences_myUserPreferences; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/scanAllMutation.ts b/ui/src/Pages/SettingsPage/__generated__/scanAllMutation.ts new file mode 100644 index 0000000..95fa9b3 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/scanAllMutation.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: scanAllMutation +// ==================================================== + +export interface scanAllMutation_scanAll { + __typename: "ScannerResult"; + success: boolean; + message: string | null; +} + +export interface scanAllMutation { + /** + * Scan all users for new media + */ + scanAll: scanAllMutation_scanAll; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/scanIntervalQuery.ts b/ui/src/Pages/SettingsPage/__generated__/scanIntervalQuery.ts new file mode 100644 index 0000000..9128437 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/scanIntervalQuery.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: scanIntervalQuery +// ==================================================== + +export interface scanIntervalQuery_siteInfo { + __typename: "SiteInfo"; + /** + * How often automatic scans should be initiated in seconds + */ + periodicScanInterval: number; +} + +export interface scanIntervalQuery { + siteInfo: scanIntervalQuery_siteInfo; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/setConcurrentWorkers.ts b/ui/src/Pages/SettingsPage/__generated__/setConcurrentWorkers.ts new file mode 100644 index 0000000..7c6207c --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/setConcurrentWorkers.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: setConcurrentWorkers +// ==================================================== + +export interface setConcurrentWorkers { + /** + * Set max number of concurrent scanner jobs running at once + */ + setScannerConcurrentWorkers: number; +} + +export interface setConcurrentWorkersVariables { + workers: number; +} diff --git a/ui/src/Pages/SharePage/AlbumSharePage.tsx b/ui/src/Pages/SharePage/AlbumSharePage.tsx new file mode 100644 index 0000000..7ff7687 --- /dev/null +++ b/ui/src/Pages/SharePage/AlbumSharePage.tsx @@ -0,0 +1,161 @@ +import React from 'react' +import Layout from '../../components/layout/Layout' +import AlbumGallery from '../../components/albumGallery/AlbumGallery' +import styled from 'styled-components' +import { gql, useQuery } from '@apollo/client' +import { useTranslation } from 'react-i18next' +import useURLParameters from '../../hooks/useURLParameters' +import useOrderingParams from '../../hooks/useOrderingParams' +import { shareAlbumQuery } from './__generated__/shareAlbumQuery' +import useScrollPagination from '../../hooks/useScrollPagination' +import PaginateLoader from '../../components/PaginateLoader' + +export const SHARE_ALBUM_QUERY = gql` + query shareAlbumQuery( + $id: ID! + $token: String! + $password: String + $mediaOrderBy: String + $mediaOrderDirection: OrderDirection + $limit: Int + $offset: Int + ) { + album(id: $id, tokenCredentials: { token: $token, password: $password }) { + id + title + subAlbums(order: { order_by: "title" }) { + id + title + thumbnail { + id + thumbnail { + url + } + } + } + media( + paginate: { limit: $limit, offset: $offset } + order: { + order_by: $mediaOrderBy + order_direction: $mediaOrderDirection + } + ) { + id + title + type + blurhash + thumbnail { + url + width + height + } + downloads { + title + mediaUrl { + url + width + height + fileSize + } + } + highRes { + url + width + height + } + videoWeb { + url + } + exif { + id + description + camera + maker + lens + dateShot + exposure + aperture + iso + focalLength + flash + exposureProgram + coordinates { + latitude + longitude + } + } + } + } + } +` + +const AlbumSharePageWrapper = styled.div` + height: 100%; +` + +type AlbumSharePageProps = { + albumID: string + token: string + password: string | null +} + +const AlbumSharePage = ({ albumID, token, password }: AlbumSharePageProps) => { + const { t } = useTranslation() + + const urlParams = useURLParameters() + const orderParams = useOrderingParams(urlParams) + + const { data, error, loading, fetchMore } = useQuery( + SHARE_ALBUM_QUERY, + { + variables: { + id: albumID, + token, + password, + limit: 200, + offset: 0, + mediaOrderBy: orderParams.orderBy, + mediaOrderDirection: orderParams.orderDirection, + }, + } + ) + + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.album.media, + }) + + if (error) { + return
    {error.message}
    + } + + const album = data?.album + + return ( + + + `/share/${token}/${albumId}`} + showFilter={false} + setOrdering={orderParams.setOrdering} + ordering={orderParams} + /> + + + + ) +} + +export default AlbumSharePage diff --git a/ui/src/Pages/SharePage/MediaSharePage.tsx b/ui/src/Pages/SharePage/MediaSharePage.tsx new file mode 100644 index 0000000..e7a44e2 --- /dev/null +++ b/ui/src/Pages/SharePage/MediaSharePage.tsx @@ -0,0 +1,64 @@ +import React, { useContext, useEffect } from 'react' +import styled from 'styled-components' +import Layout from '../../components/layout/Layout' +import { + ProtectedImage, + ProtectedVideo, +} from '../../components/photoGallery/ProtectedMedia' +import { SidebarContext } from '../../components/sidebar/Sidebar' +import MediaSidebar from '../../components/sidebar/MediaSidebar/MediaSidebar' +import { useTranslation } from 'react-i18next' +import { SharePageToken_shareToken_media } from './__generated__/SharePageToken' +import { MediaType } from '../../__generated__/globalTypes' +import { exhaustiveCheck } from '../../helpers/utils' + +const DisplayPhoto = styled(ProtectedImage)` + /* width: 100%; */ + max-height: calc(80vh); + object-fit: contain; +` + +const DisplayVideo = styled(ProtectedVideo)` + /* width: 100%; */ + max-height: calc(80vh); +` + +type MediaViewProps = { + media: SharePageToken_shareToken_media +} + +const MediaView = ({ media }: MediaViewProps) => { + const { updateSidebar } = useContext(SidebarContext) + + useEffect(() => { + updateSidebar() + }, [media]) + + switch (media.type) { + case MediaType.Photo: + return + case MediaType.Video: + return + } + + exhaustiveCheck(media.type) +} + +type MediaSharePageType = { + media: SharePageToken_shareToken_media +} + +const MediaSharePage = ({ media }: MediaSharePageType) => { + const { t } = useTranslation() + + return ( + +
    +

    {media.title}

    + +
    +
    + ) +} + +export default MediaSharePage diff --git a/ui/src/Pages/SharePage/PasswordProtectedShare.tsx b/ui/src/Pages/SharePage/PasswordProtectedShare.tsx new file mode 100644 index 0000000..f126252 --- /dev/null +++ b/ui/src/Pages/SharePage/PasswordProtectedShare.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react' +import { useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { TextField } from '../../primitives/form/Input' +import { MessageContainer } from './SharePage' + +type ProtectedTokenEnterPasswordProps = { + refetchWithPassword(password: string): void + loading: boolean +} + +const PasswordProtectedShare = ({ + refetchWithPassword, + loading = false, +}: ProtectedTokenEnterPasswordProps) => { + const { t } = useTranslation() + + const { + register, + watch, + formState: { errors }, + handleSubmit, + } = useForm() + + const [invalidPassword, setInvalidPassword] = useState(false) + + const onSubmit = () => { + refetchWithPassword(watch('password') as string) + setInvalidPassword(true) + } + + let errorMessage = undefined + if (invalidPassword && !loading) { + errorMessage = t( + 'share_page.wrong_password', + 'Wrong password, please try again.' + ) + } else if (errors.password) { + errorMessage = t( + 'share_page.protected_share.password_required_error', + 'Password is required' + ) + } + + return ( + +

    + {t('share_page.protected_share.title', 'Protected share')} +

    +

    + {t( + 'share_page.protected_share.description', + 'This share is protected with a password.' + )} +

    + +
    + ) +} + +export default PasswordProtectedShare diff --git a/ui/src/Pages/SharePage/SharePage.test.tsx b/ui/src/Pages/SharePage/SharePage.test.tsx new file mode 100644 index 0000000..3366177 --- /dev/null +++ b/ui/src/Pages/SharePage/SharePage.test.tsx @@ -0,0 +1,377 @@ +import React from 'react' +import { MemoryRouter, Routes, Route } from 'react-router-dom' +import { MockedProvider } from '@apollo/client/testing' + +import { + render, + screen, + waitForElementToBeRemoved, +} from '@testing-library/react' + +import {clearSharePassword, saveSharePassword} from '../../helpers/authentication' + +import { + SHARE_TOKEN_QUERY, + TokenRoute, + VALIDATE_TOKEN_PASSWORD_QUERY, +} from './SharePage' + +import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownloadMedia' +import { SHARE_ALBUM_QUERY } from './AlbumSharePage' + + +vi.mock('../../hooks/useScrollPagination') + +describe('load correct share page, based on graphql query', () => { + const token = 'TOKEN123' + const password = 'PASSWORD-123_@456\\' + + const historyMock = [{ pathname: `/share/${token}` }] + + const graphqlMocks = [ + { + request: { + query: VALIDATE_TOKEN_PASSWORD_QUERY, + variables: { + token, + password, + }, + }, + result: { + data: { + shareTokenValidatePassword: true, + }, + }, + }, + { + request: { + query: SIDEBAR_DOWNLOAD_QUERY, + variables: { + mediaId: '1', + }, + }, + result: { + data: { + media: { + id: '1', + downloads: [], + }, + }, + }, + }, + ] + + beforeAll(() => { + saveSharePassword(token, password) + }) + + afterAll(() => { + clearSharePassword(token) + }) + + test('load media share page', async () => { + const mediaPageMock = { + request: { + query: SHARE_TOKEN_QUERY, + variables: { + token, + password, + }, + }, + result: { + data: { + shareToken: { + token: token, + album: null, + media: { + id: '1', + title: 'shared_image.jpg', + type: 'Photo', + highRes: { + url: 'https://example.com/shared_image.jpg', + }, + }, + }, + }, + }, + } + + render( + + + + } /> + + + + ) + + expect(screen.getByText('Loading...')).toBeInTheDocument() + + await waitForElementToBeRemoved(() => screen.queryByText('Loading...')) + + expect(screen.getByTestId('Layout')).toBeInTheDocument() + expect(screen.getByTestId('MediaSharePage')).toBeInTheDocument() + }) + + test('load album share page', async () => { + const albumPageMock = [ + { + request: { + query: SHARE_TOKEN_QUERY, + variables: { + token, + password, + }, + }, + result: { + data: { + shareToken: { + token: token, + album: { + id: '1', + }, + media: null, + }, + }, + }, + }, + { + request: { + query: SHARE_ALBUM_QUERY, + variables: { + id: '1', + token, + password, + limit: 200, + offset: 0, + mediaOrderBy: 'date_shot', + mediaOrderDirection: 'ASC', + }, + }, + result: { + data: { + album: { + id: '1', + title: 'album_title', + subAlbums: [], + thumbnail: { + url: 'https://photoview.example.com/album_thumbnail.jpg', + }, + media: [], + }, + }, + }, + }, + ] + + render( + + + + } /> + + + + ) + + expect(screen.getByText('Loading...')).toBeInTheDocument() + await waitForElementToBeRemoved(() => screen.getByText('Loading...')) + + expect(screen.getByTestId('Layout')).toBeInTheDocument() + expect(screen.getByTestId('AlbumSharePage')).toBeInTheDocument() + }) + + test('load subalbum of a shared album', async () => { + const subalbumID = '456' + const subalbumHistoryMock = [{ pathname: `/share/${token}/${subalbumID}` }] + + const subalbumPageMocks = [ + { + request: { + query: SHARE_TOKEN_QUERY, + variables: { + token, + password, + }, + }, + result: { + data: { + shareToken: { + token: token, + album: { + id: subalbumID, + }, + media: null, + }, + }, + }, + }, + { + request: { + query: SHARE_ALBUM_QUERY, + variables: { + id: subalbumID, + token, + password, + limit: 200, + offset: 0, + mediaOrderBy: 'date_shot', + mediaOrderDirection: 'ASC', + }, + }, + result: { + data: { + album: { + id: '1', + title: 'album_title', + subAlbums: [], + thumbnail: { + url: 'https://photoview.example.com/album_thumbnail.jpg', + }, + media: [], + }, + }, + }, + }, + ] + + render( + + + + } /> + + + + ) + + expect(screen.getByText('Loading...')).toBeInTheDocument() + await waitForElementToBeRemoved(() => screen.getByText('Loading...')) + + expect(screen.getByTestId('Layout')).toBeInTheDocument() + expect(screen.getByTestId('AlbumSharePage')).toBeInTheDocument() + }) + + test(`test the expired`, async () => { + const expiredValidationMock = { + request: { + query: VALIDATE_TOKEN_PASSWORD_QUERY, + variables: { token: 'TOKEN123', password: "PASSWORD-123_@456\\" }, + }, + error: new Error('share expired'), + }; + + render( + + + + } /> + + + + ) + expect(screen.getByText('Loading...')).toBeInTheDocument() + + await waitForElementToBeRemoved(() => screen.queryByText('Loading...')) + + expect(screen.getByText(/share expired/i)).toBeInTheDocument() + }) + +test(`test the normal`, async () => { + + const shareTokenMock = { + request: { + query: SHARE_TOKEN_QUERY, + variables: { token: 'TOKEN123', password: "PASSWORD-123_@456\\" }, + }, + result: { + data: { + shareToken: { + token: 'TOKEN123', + album: { id: '1' }, + media: null, + }, + }, + }, + }; + + const albumDataMock = { + request: { + query: SHARE_ALBUM_QUERY, + variables: { + id: '1', + token: 'TOKEN123', + password: "PASSWORD-123_@456\\", + limit: 200, + offset: 0, + mediaOrderBy: 'date_shot', + mediaOrderDirection: 'ASC', + }, + }, + result: { + data: { + album: { + id: '1', + title: 'normal_album', + subAlbums: [], + thumbnail: { url: '...' }, + media: [], + }, + }, + }, + }; + + render( + + + + } /> + + + + ) + + expect(screen.getByText('Loading...')).toBeInTheDocument() + await waitForElementToBeRemoved(() => screen.queryByText('Loading...')) + + expect(screen.getByTestId('Layout')).toBeInTheDocument() + expect(screen.getByTestId('AlbumSharePage')).toBeInTheDocument() +}) + + +}) diff --git a/ui/src/Pages/SharePage/SharePage.tsx b/ui/src/Pages/SharePage/SharePage.tsx new file mode 100644 index 0000000..d93fb0a --- /dev/null +++ b/ui/src/Pages/SharePage/SharePage.tsx @@ -0,0 +1,202 @@ +import React from 'react' +import { useQuery, gql } from '@apollo/client' +import { Route, Routes, useParams } from 'react-router-dom' +import styled from 'styled-components' +import { + getSharePassword, + saveSharePassword, +} from '../../helpers/authentication' +import AlbumSharePage from './AlbumSharePage' +import MediaSharePage from './MediaSharePage' +import { useTranslation } from 'react-i18next' +import PasswordProtectedShare from './PasswordProtectedShare' +import { isNil } from '../../helpers/utils' +import { + SharePageToken, + SharePageTokenVariables, +} from './__generated__/SharePageToken' +import { + ShareTokenValidatePassword, + ShareTokenValidatePasswordVariables, +} from './__generated__/ShareTokenValidatePassword' + +export const SHARE_TOKEN_QUERY = gql` + query SharePageToken($token: String!, $password: String) { + shareToken(credentials: { token: $token, password: $password }) { + token + album { + id + } + media { + id + title + type + thumbnail { + url + width + height + } + downloads { + title + mediaUrl { + url + width + height + fileSize + } + } + highRes { + url + width + height + } + videoWeb { + url + width + height + } + exif { + id + description + camera + maker + lens + dateShot + exposure + aperture + iso + focalLength + flash + exposureProgram + coordinates { + longitude + latitude + } + } + } + } + } +` + +export const VALIDATE_TOKEN_PASSWORD_QUERY = gql` + query ShareTokenValidatePassword($token: String!, $password: String) { + shareTokenValidatePassword( + credentials: { token: $token, password: $password } + ) + } +` + +const tokenFromParams = () => { + const { token } = useParams() + if (isNil(token)) throw new Error('Expected `token` param to be defined') + return token +} + +const AuthorizedTokenRoute = () => { + const { t } = useTranslation() + + const token = tokenFromParams() + const password = getSharePassword(token) + + const { loading, error, data } = useQuery< + SharePageToken, + SharePageTokenVariables + >(SHARE_TOKEN_QUERY, { + variables: { + token, + password, + }, + }) + + if (!isNil(error)) return
    {error.message}
    + if (loading) return
    {t('general.loading.default', 'Loading...')}
    + + if (data?.shareToken.album) { + const SharedSubAlbumPage = () => { + const { subAlbum } = useParams() + if (isNil(subAlbum)) + throw new Error('Expected `subAlbum` param to be defined') + + return ( + + ) + } + + return ( + + } /> + + } + /> + + ) + } + + if (data?.shareToken.media) { + return + } + + return

    {t('share_page.share_not_found', 'Share not found')}

    +} + +export const MessageContainer = styled.div` + max-width: 400px; + margin: 100px auto 0; +` + +export const TokenRoute = () => { + const { t } = useTranslation() + const token = tokenFromParams() + + const { loading, error, data, refetch } = useQuery< + ShareTokenValidatePassword, + ShareTokenValidatePasswordVariables + >(VALIDATE_TOKEN_PASSWORD_QUERY, { + notifyOnNetworkStatusChange: true, + variables: { + token: token, + password: getSharePassword(token), + }, + }) + + if (error) { + if (error.message == 'GraphQL error: share not found') { + return ( + +

    {t('share_page.share_not_found', 'Share not found')}

    +

    + {t( + 'share_page.share_not_found_description', + 'Maybe the share has expired or has been deleted.' + )} +

    +
    + ) + } + + return
    {error.message}
    + } + + if (data && data.shareTokenValidatePassword == false) { + return ( + { + saveSharePassword(token, password) + refetch({ token, password }) + }} + loading={loading} + /> + ) + } + + if (loading) return
    {t('general.loading.default', 'Loading...')}
    + + return +} diff --git a/ui/src/Pages/SharePage/__generated__/SharePageToken.ts b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts new file mode 100644 index 0000000..366cc78 --- /dev/null +++ b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts @@ -0,0 +1,206 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: SharePageToken +// ==================================================== + +export interface SharePageToken_shareToken_album { + __typename: "Album"; + id: string; +} + +export interface SharePageToken_shareToken_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface SharePageToken_shareToken_media_downloads_mediaUrl { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; + /** + * The file size of the resource in bytes + */ + fileSize: number; +} + +export interface SharePageToken_shareToken_media_downloads { + __typename: "MediaDownload"; + /** + * A description of the role of the media file + */ + title: string; + mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl; +} + +export interface SharePageToken_shareToken_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface SharePageToken_shareToken_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface SharePageToken_shareToken_media_exif_coordinates { + __typename: "Coordinates"; + /** + * GPS longitude in degrees + */ + longitude: number; + /** + * GPS latitude in degrees + */ + latitude: number; +} + +export interface SharePageToken_shareToken_media_exif { + __typename: "MediaEXIF"; + id: string; + /** + * The description of the image + */ + description: string | null; + /** + * The model name of the camera + */ + camera: string | null; + /** + * The maker of the camera + */ + maker: string | null; + /** + * The name of the lens + */ + lens: string | null; + /** + * The date when the photo is shot + */ + dateShot: string | null; + /** + * The exposure time of the image + */ + exposure: number | null; + /** + * The aperature stops of the image + */ + aperture: number | null; + /** + * The ISO setting of the image + */ + iso: number | null; + /** + * The focal length of the lens, when the image was taken + */ + focalLength: number | null; + /** + * A formatted description of the flash settings, when the image was taken + */ + flash: number | null; + /** + * An index describing the mode for adjusting the exposure of the image + */ + exposureProgram: number | null; + /** + * GPS coordinates of where the image was taken + */ + coordinates: SharePageToken_shareToken_media_exif_coordinates | null; +} + +export interface SharePageToken_shareToken_media { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: SharePageToken_shareToken_media_thumbnail | null; + /** + * A list of different versions of files for this media that can be downloaded by the user + */ + downloads: SharePageToken_shareToken_media_downloads[]; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: SharePageToken_shareToken_media_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: SharePageToken_shareToken_media_videoWeb | null; + exif: SharePageToken_shareToken_media_exif | null; +} + +export interface SharePageToken_shareToken { + __typename: "ShareToken"; + token: string; + /** + * The album this token shares + */ + album: SharePageToken_shareToken_album | null; + /** + * The media this token shares + */ + media: SharePageToken_shareToken_media | null; +} + +export interface SharePageToken { + /** + * Fetch a share token containing an `Album` or `Media` + */ + shareToken: SharePageToken_shareToken; +} + +export interface SharePageTokenVariables { + token: string; + password?: string | null; +} diff --git a/ui/src/Pages/SharePage/__generated__/ShareTokenValidatePassword.ts b/ui/src/Pages/SharePage/__generated__/ShareTokenValidatePassword.ts new file mode 100644 index 0000000..e1c6d56 --- /dev/null +++ b/ui/src/Pages/SharePage/__generated__/ShareTokenValidatePassword.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: ShareTokenValidatePassword +// ==================================================== + +export interface ShareTokenValidatePassword { + /** + * Check if the `ShareToken` credentials are valid + */ + shareTokenValidatePassword: boolean; +} + +export interface ShareTokenValidatePasswordVariables { + token: string; + password?: string | null; +} diff --git a/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts new file mode 100644 index 0000000..5aa6296 --- /dev/null +++ b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts @@ -0,0 +1,231 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { OrderDirection, MediaType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: shareAlbumQuery +// ==================================================== + +export interface shareAlbumQuery_album_subAlbums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface shareAlbumQuery_album_subAlbums_thumbnail { + __typename: "Media"; + id: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null; +} + +export interface shareAlbumQuery_album_subAlbums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null; +} + +export interface shareAlbumQuery_album_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface shareAlbumQuery_album_media_downloads_mediaUrl { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; + /** + * The file size of the resource in bytes + */ + fileSize: number; +} + +export interface shareAlbumQuery_album_media_downloads { + __typename: "MediaDownload"; + /** + * A description of the role of the media file + */ + title: string; + mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl; +} + +export interface shareAlbumQuery_album_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface shareAlbumQuery_album_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface shareAlbumQuery_album_media_exif_coordinates { + __typename: "Coordinates"; + /** + * GPS latitude in degrees + */ + latitude: number; + /** + * GPS longitude in degrees + */ + longitude: number; +} + +export interface shareAlbumQuery_album_media_exif { + __typename: "MediaEXIF"; + id: string; + /** + * The description of the image + */ + description: string | null; + /** + * The model name of the camera + */ + camera: string | null; + /** + * The maker of the camera + */ + maker: string | null; + /** + * The name of the lens + */ + lens: string | null; + /** + * The date when the photo is shot + */ + dateShot: string | null; + /** + * The exposure time of the image + */ + exposure: number | null; + /** + * The aperature stops of the image + */ + aperture: number | null; + /** + * The ISO setting of the image + */ + iso: number | null; + /** + * The focal length of the lens, when the image was taken + */ + focalLength: number | null; + /** + * A formatted description of the flash settings, when the image was taken + */ + flash: number | null; + /** + * An index describing the mode for adjusting the exposure of the image + */ + exposureProgram: number | null; + /** + * GPS coordinates of where the image was taken + */ + coordinates: shareAlbumQuery_album_media_exif_coordinates | null; +} + +export interface shareAlbumQuery_album_media { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * A short string that can be used to generate a blured version of the media, to show while the original is loading + */ + blurhash: string | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: shareAlbumQuery_album_media_thumbnail | null; + /** + * A list of different versions of files for this media that can be downloaded by the user + */ + downloads: shareAlbumQuery_album_media_downloads[]; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: shareAlbumQuery_album_media_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: shareAlbumQuery_album_media_videoWeb | null; + exif: shareAlbumQuery_album_media_exif | null; +} + +export interface shareAlbumQuery_album { + __typename: "Album"; + id: string; + title: string; + /** + * The albums contained in this album + */ + subAlbums: shareAlbumQuery_album_subAlbums[]; + /** + * The media inside this album + */ + media: shareAlbumQuery_album_media[]; +} + +export interface shareAlbumQuery { + /** + * Get album by id, user must own the album or be admin + * If valid tokenCredentials are provided, the album may be retrived without further authentication + */ + album: shareAlbumQuery_album; +} + +export interface shareAlbumQueryVariables { + id: string; + token: string; + password?: string | null; + mediaOrderBy?: string | null; + mediaOrderDirection?: OrderDirection | null; + limit?: number | null; + offset?: number | null; +} diff --git a/ui/src/Pages/TimelinePage/TimelinePage.tsx b/ui/src/Pages/TimelinePage/TimelinePage.tsx new file mode 100644 index 0000000..47f666c --- /dev/null +++ b/ui/src/Pages/TimelinePage/TimelinePage.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import Layout from '../../components/layout/Layout' +import { useTranslation } from 'react-i18next' +import TimelineGallery from '../../components/timelineGallery/TimelineGallery' + +const TimelinePage = () => { + const { t } = useTranslation() + + return ( + <> + + + + + ) +} + +export default TimelinePage diff --git a/ui/src/__generated__/adminQuery.ts b/ui/src/__generated__/adminQuery.ts new file mode 100644 index 0000000..2301f23 --- /dev/null +++ b/ui/src/__generated__/adminQuery.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: adminQuery +// ==================================================== + +export interface adminQuery_myUser { + __typename: 'User' + admin: boolean +} + +export interface adminQuery { + /** + * Information about the currently logged in user + */ + myUser: adminQuery_myUser +} diff --git a/ui/src/__generated__/globalTypes.ts b/ui/src/__generated__/globalTypes.ts new file mode 100644 index 0000000..228d55a --- /dev/null +++ b/ui/src/__generated__/globalTypes.ts @@ -0,0 +1,58 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +//============================================================== +// START Enums and Input Objects +//============================================================== + +/** + * Supported language translations of the user interface + */ +export enum LanguageTranslation { + Basque = "Basque", + Danish = "Danish", + Dutch = "Dutch", + English = "English", + French = "French", + German = "German", + Italian = "Italian", + Japanese = "Japanese", + Polish = "Polish", + Portuguese = "Portuguese", + Russian = "Russian", + SimplifiedChinese = "SimplifiedChinese", + Spanish = "Spanish", + Swedish = "Swedish", + TraditionalChineseHK = "TraditionalChineseHK", + TraditionalChineseTW = "TraditionalChineseTW", + Turkish = "Turkish", + Ukrainian = "Ukrainian", +} + +export enum MediaType { + Photo = "Photo", + Video = "Video", +} + +/** + * Specified the type a particular notification is of + */ +export enum NotificationType { + Close = "Close", + Message = "Message", + Progress = "Progress", +} + +/** + * Used to specify which order to sort items in + */ +export enum OrderDirection { + ASC = "ASC", + DESC = "DESC", +} + +//============================================================== +// END Enums and Input Objects +//============================================================== diff --git a/ui/src/__generated__/mapboxEnabledQuery.ts b/ui/src/__generated__/mapboxEnabledQuery.ts new file mode 100644 index 0000000..0600784 --- /dev/null +++ b/ui/src/__generated__/mapboxEnabledQuery.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mapboxEnabledQuery +// ==================================================== + +export interface mapboxEnabledQuery { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null +} diff --git a/ui/src/__generated__/siteTranslation.ts b/ui/src/__generated__/siteTranslation.ts new file mode 100644 index 0000000..5bdca45 --- /dev/null +++ b/ui/src/__generated__/siteTranslation.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { LanguageTranslation } from "./globalTypes"; + +// ==================================================== +// GraphQL query operation: siteTranslation +// ==================================================== + +export interface siteTranslation_myUserPreferences { + __typename: "UserPreferences"; + id: string; + language: LanguageTranslation | null; +} + +export interface siteTranslation { + /** + * User preferences for the logged in user + */ + myUserPreferences: siteTranslation_myUserPreferences; +} diff --git a/ui/src/apolloClient.ts b/ui/src/apolloClient.ts new file mode 100644 index 0000000..3b31a92 --- /dev/null +++ b/ui/src/apolloClient.ts @@ -0,0 +1,195 @@ +import { + InMemoryCache, + ApolloClient, + split, + ApolloLink, + HttpLink, + ServerError, + FieldMergeFunction, +} from '@apollo/client' +import { getMainDefinition } from '@apollo/client/utilities' +import { onError } from '@apollo/client/link/error' +import { WebSocketLink } from '@apollo/client/link/ws' + +import urlJoin from 'url-join' +import { authToken, clearTokenCookie } from './helpers/authentication' +import { MessageState } from './components/messages/Messages' +import { Message } from './components/messages/SubscriptionsHook' +import { NotificationType } from './__generated__/globalTypes' + +export const API_ENDPOINT = import.meta.env.REACT_APP_API_ENDPOINT + ? (import.meta.env.REACT_APP_API_ENDPOINT as string) + : urlJoin(location.origin, '/api') + +export const GRAPHQL_ENDPOINT = urlJoin(API_ENDPOINT, '/graphql') + +const httpLink = new HttpLink({ + uri: GRAPHQL_ENDPOINT, + credentials: 'include', +}) + +const apiProtocol = new URL(GRAPHQL_ENDPOINT).protocol + +const websocketUri = new URL(GRAPHQL_ENDPOINT) +websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:' + +const wsLink = new WebSocketLink({ + uri: websocketUri.toString(), + options: { + reconnect: true, + lazy: true, + connectionParams: () => { + const token = authToken() + if (token) { + return { + Authorization: `Bearer ${token}`, + } + } + return {} + }, + }, +}) + +const link = split( + // split based on operation type + ({ query }) => { + const definition = getMainDefinition(query) + return ( + definition.kind === 'OperationDefinition' && + definition.operation === 'subscription' + ) + }, + wsLink, + httpLink +) + +const linkError = onError(({ graphQLErrors, networkError }) => { + const errorMessages = [] + + const formatPath = (path: readonly (string | number)[] | undefined) => + path?.join('::') ?? 'undefined' + + if (graphQLErrors) { + graphQLErrors.map(({ message, locations, path }) => + console.log( + `[GraphQL error]: Message: ${message}, Location: ${JSON.stringify( + locations + )} Path: ${formatPath(path)}` + ) + ) + + if (graphQLErrors.length == 1) { + errorMessages.push({ + header: 'Something went wrong', + content: `Server error: ${graphQLErrors[0].message} at (${formatPath( + graphQLErrors[0].path + )})`, + }) + } else if (graphQLErrors.length > 1) { + errorMessages.push({ + header: 'Multiple things went wrong', + content: `Received ${graphQLErrors.length} errors from the server. See the console for more information`, + }) + } + + if (graphQLErrors.find(x => x.message == 'unauthorized')) { + console.log('Unauthorized, clearing token cookie') + clearTokenCookie() + // location.reload() + } + } + + if (networkError) { + console.log(`[Network error]: ${JSON.stringify(networkError)}`) + clearTokenCookie() + + const errors = + ((networkError as ServerError)?.result.errors as Error[]) || [] + + if (errors.length == 1) { + errorMessages.push({ + header: 'Server error', + content: `You are being logged out in an attempt to recover.\n${errors[0].message}`, + }) + } else if (errors.length > 1) { + errorMessages.push({ + header: 'Multiple server errors', + content: `Received ${ + graphQLErrors?.length || 0 + } errors from the server. You are being logged out in an attempt to recover.`, + }) + } + } + + if (errorMessages.length > 0) { + const newMessages: Message[] = errorMessages.map(msg => ({ + key: Math.random().toString(26), + type: NotificationType.Message, + props: { + negative: true, + ...msg, + }, + })) + MessageState.set((messages: Message[]) => [...messages, ...newMessages]) + } +}) + +type PaginateCacheType = { + keyArgs: string[] + merge: FieldMergeFunction +} + +// Modified version of Apollo's offsetLimitPagination() +const paginateCache = (keyArgs: string[]) => + ({ + keyArgs, + merge(existing, incoming, { args, fieldName }) { + const merged = existing ? existing.slice(0) : [] + if (args?.paginate) { + const { offset = 0 } = args.paginate as { offset: number } + for (let i = 0; i < incoming.length; ++i) { + merged[offset + i] = incoming[i] + } + } else { + throw new Error(`Paginate argument is missing for query: ${fieldName}`) + } + return merged + }, + }) as PaginateCacheType + +const memoryCache = new InMemoryCache({ + typePolicies: { + // There only exists one global instance of SiteInfo, + // therefore it can always be merged + SiteInfo: { + merge: true, + }, + MediaURL: { + keyFields: ['url'], + }, + Album: { + fields: { + media: paginateCache(['onlyFavorites', 'order']), + }, + }, + FaceGroup: { + fields: { + imageFaces: paginateCache([]), + }, + }, + Query: { + fields: { + myTimeline: paginateCache(['onlyFavorites']), + myFaceGroups: paginateCache([]), + }, + }, + }, +}) + +const client = new ApolloClient({ + // link: ApolloLink.from([linkError, authLink.concat(link)]), + link: ApolloLink.from([linkError, link]), + cache: memoryCache, +}) + +export default client diff --git a/ui/src/components/PaginateLoader.tsx b/ui/src/components/PaginateLoader.tsx new file mode 100644 index 0000000..81c5fd4 --- /dev/null +++ b/ui/src/components/PaginateLoader.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import Loader from '../primitives/Loader' + +type PaginateLoaderProps = { + active: boolean + text: string +} + +const PaginateLoader = ({ active, text }: PaginateLoaderProps) => ( + +) + +export default PaginateLoader diff --git a/ui/src/components/__generated__/albumPathQuery.ts b/ui/src/components/__generated__/albumPathQuery.ts new file mode 100644 index 0000000..6d141f5 --- /dev/null +++ b/ui/src/components/__generated__/albumPathQuery.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: albumPathQuery +// ==================================================== + +export interface albumPathQuery_album_path { + __typename: 'Album' + id: string + title: string +} + +export interface albumPathQuery_album { + __typename: 'Album' + id: string + path: albumPathQuery_album_path[] +} + +export interface albumPathQuery { + /** + * Get album by id, user must own the album or be admin + * If valid tokenCredentials are provided, the album may be retrived without further authentication + */ + album: albumPathQuery_album +} + +export interface albumPathQueryVariables { + id: string +} diff --git a/ui/src/components/album/AlbumFilter.tsx b/ui/src/components/album/AlbumFilter.tsx new file mode 100644 index 0000000..7aa62a4 --- /dev/null +++ b/ui/src/components/album/AlbumFilter.tsx @@ -0,0 +1,165 @@ +import React from 'react' +import { authToken } from '../../helpers/authentication' +import { useTranslation } from 'react-i18next' +import { OrderDirection } from '../../__generated__/globalTypes' +import { MediaOrdering, SetOrderingFn } from '../../hooks/useOrderingParams' +import Checkbox from '../../primitives/form/Checkbox' + +import { ReactComponent as SortingIcon } from './icons/sorting.svg' +import { ReactComponent as DirectionIcon } from './icons/direction-arrow.svg' + +import Dropdown from '../../primitives/form/Dropdown' +import classNames from 'classnames' + +export type SortingOptionValue = 'date_shot' | 'updated_at' | 'title' | 'type' +export type SortingOption = { value: SortingOptionValue; label: string } + +export type FavoriteCheckboxProps = { + onlyFavorites: boolean + setOnlyFavorites(favorites: boolean): void +} + +export const FavoritesCheckbox = ({ + onlyFavorites, + setOnlyFavorites, +}: FavoriteCheckboxProps) => { + const { t } = useTranslation() + + return ( + setOnlyFavorites(e.target.checked)} + /> + ) +} + +type SortingOptionsProps = { + ordering?: MediaOrdering + setOrdering?: SetOrderingFn + items?: SortingOption[] +} + +const SortingOptions = ({ + setOrdering, + ordering, + items, +}: SortingOptionsProps) => { + const { t } = useTranslation() + + const changeOrderDirection = () => { + if (setOrdering && ordering) { + setOrdering({ + orderDirection: + ordering.orderDirection === OrderDirection.ASC + ? OrderDirection.DESC + : OrderDirection.ASC, + }) + } + } + + const changeOrderBy = (value: SortingOptionValue) => { + if (setOrdering) { + setOrdering({ orderBy: value }) + } + } + + const defaultOptions = React.useMemo( + () => [ + { + value: 'date_shot', + label: t('album_filter.sorting_options.date_shot', 'Date shot'), + }, + { + value: 'updated_at', + label: t('album_filter.sorting_options.date_imported', 'Date imported'), + }, + { + value: 'title', + label: t('album_filter.sorting_options.title', 'Title'), + }, + { + value: 'type', + label: t('album_filter.sorting_options.type', 'Kind'), + }, + ], + [t] + ) + + const sortingOptions = items ?? defaultOptions + + return ( +
    + + +
    + + +
    +
    + ) +} + +type AlbumFilterProps = { + onlyFavorites: boolean + setOnlyFavorites?(favorites: boolean): void + ordering?: MediaOrdering + setOrdering?: SetOrderingFn + sortingOptions?: SortingOption[] +} + +const AlbumFilter = ({ + onlyFavorites, + setOnlyFavorites, + setOrdering, + ordering, + sortingOptions, +}: AlbumFilterProps) => { + return ( +
    + {ordering && setOrdering ? ( + + ) : null} + {authToken() && setOnlyFavorites && ( + + )} +
    + ) +} + +export default AlbumFilter diff --git a/ui/src/components/album/AlbumTitle.tsx b/ui/src/components/album/AlbumTitle.tsx new file mode 100644 index 0000000..4774b94 --- /dev/null +++ b/ui/src/components/album/AlbumTitle.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useContext } from 'react' +import { Link } from 'react-router-dom' +import styled from 'styled-components' +import { SidebarContext } from '../sidebar/Sidebar' +import AlbumSidebar from '../sidebar/AlbumSidebar' +import { useLazyQuery, gql } from '@apollo/client' +import { authToken } from '../../helpers/authentication' +import { albumPathQuery } from './__generated__/albumPathQuery' +import useDelay from '../../hooks/useDelay' + +import { ReactComponent as GearIcon } from './icons/gear.svg' +import { tailwindClassNames } from '../../helpers/utils' +import { buttonStyles } from '../../primitives/form/Input' + +export const BreadcrumbList = styled.ol<{ hideLastArrow?: boolean }>` + & + ${({ hideLastArrow }) => + hideLastArrow ? 'li:not(:last-child)::after' : 'li::after'} { + content: ''; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='5px' height='6px' viewBox='0 0 5 6'%3E%3Cpolyline fill='none' stroke='%23979797' points='0.74 0.167710644 3.57228936 3 0.74 5.83228936' /%3E%3C/svg%3E"); + width: 5px; + height: 6px; + display: inline-block; + margin: 6px; + vertical-align: middle; + } +` + +const ALBUM_PATH_QUERY = gql` + query albumPathQuery($id: ID!) { + album(id: $id) { + id + path { + id + title + } + } + } +` + +type AlbumTitleProps = { + album?: { + id: string + title: string + } + disableLink: boolean +} + +const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => { + const [fetchPath, { data: pathData }] = + useLazyQuery(ALBUM_PATH_QUERY) + const { updateSidebar } = useContext(SidebarContext) + + useEffect(() => { + if (!album) return + + if (authToken() && disableLink == true) { + fetchPath({ + variables: { + id: album.id, + }, + }) + } + }, [album]) + + const delay = useDelay(200, [album]) + + if (!album) { + return ( +
    +
    +
    +
    + ) + } + + let title = {album.title} + + const path = pathData?.album.path || [] + + const breadcrumbSections = path + .slice() + .reverse() + .map(x => ( +
  • + {x.title} +
  • + )) + + if (!disableLink) { + title = {title} + } + + return ( +
    +
    + +

    {title}

    +
    + {authToken() && ( + + )} +
    + ) +} + +export default AlbumTitle diff --git a/ui/src/components/album/__generated__/albumPathQuery.ts b/ui/src/components/album/__generated__/albumPathQuery.ts new file mode 100644 index 0000000..e4288a5 --- /dev/null +++ b/ui/src/components/album/__generated__/albumPathQuery.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: albumPathQuery +// ==================================================== + +export interface albumPathQuery_album_path { + __typename: "Album"; + id: string; + title: string; +} + +export interface albumPathQuery_album { + __typename: "Album"; + id: string; + /** + * A breadcrumb list of all parent albums down to this one + */ + path: albumPathQuery_album_path[]; +} + +export interface albumPathQuery { + /** + * Get album by id, user must own the album or be admin + * If valid tokenCredentials are provided, the album may be retrived without further authentication + */ + album: albumPathQuery_album; +} + +export interface albumPathQueryVariables { + id: string; +} diff --git a/ui/src/components/album/icons/direction-arrow.svg b/ui/src/components/album/icons/direction-arrow.svg new file mode 100644 index 0000000..1860eff --- /dev/null +++ b/ui/src/components/album/icons/direction-arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/album/icons/gear.svg b/ui/src/components/album/icons/gear.svg new file mode 100644 index 0000000..8e48f3f --- /dev/null +++ b/ui/src/components/album/icons/gear.svg @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/ui/src/components/album/icons/sorting.svg b/ui/src/components/album/icons/sorting.svg new file mode 100644 index 0000000..e2ee5c8 --- /dev/null +++ b/ui/src/components/album/icons/sorting.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/albumGallery/AlbumBox.tsx b/ui/src/components/albumGallery/AlbumBox.tsx new file mode 100644 index 0000000..06fe567 --- /dev/null +++ b/ui/src/components/albumGallery/AlbumBox.tsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react' +import { Link } from 'react-router-dom' +import { ProtectedImage } from '../photoGallery/ProtectedMedia' +import { albumQuery_album_subAlbums } from '../../Pages/AlbumPage/__generated__/albumQuery' + +interface AlbumBoxImageProps { + src?: string +} + +const AlbumBoxImage = ({ src, ...props }: AlbumBoxImageProps) => { + const [loaded, setLoaded] = useState(false) + + let image = null + if (src) { + image = ( + setLoaded(true)} + src={src} + /> + ) + } + + let placeholder = null + if (!loaded) { + placeholder = ( +
    + ) + } + + return ( +
    + {image} + {placeholder} +
    + ) +} + +type AlbumBoxProps = { + album?: albumQuery_album_subAlbums + customLink?: string +} + +export const AlbumBox = ({ album, customLink, ...props }: AlbumBoxProps) => { + const wrapperClasses = + 'inline-block text-center text-gray-900 dark:text-gray-200 mx-3 my-2 xs:h-60 xs:w-[220px]' + + if (album) { + return ( + + +

    + {album.title} +

    + + ) + } + + return ( +
    + +
    + ) +} diff --git a/ui/src/components/albumGallery/AlbumBoxes.tsx b/ui/src/components/albumGallery/AlbumBoxes.tsx new file mode 100644 index 0000000..f71de07 --- /dev/null +++ b/ui/src/components/albumGallery/AlbumBoxes.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { albumQuery_album_subAlbums } from '../../Pages/AlbumPage/__generated__/albumQuery' +import { AlbumBox } from './AlbumBox' + +type AlbumBoxesProps = { + error?: Error + albums?: albumQuery_album_subAlbums[] + getCustomLink?(albumID: string): string +} + +const AlbumBoxes = ({ error, albums, getCustomLink }: AlbumBoxesProps) => { + if (error) return
    Error {error.message}
    + + let albumElements = [] + + if (albums !== undefined) { + albumElements = albums.map(album => ( + + )) + } else { + for (let i = 0; i < 4; i++) { + albumElements.push() + } + } + + return
    {albumElements}
    +} + +export default AlbumBoxes diff --git a/ui/src/components/albumGallery/AlbumGallery.tsx b/ui/src/components/albumGallery/AlbumGallery.tsx new file mode 100644 index 0000000..f350811 --- /dev/null +++ b/ui/src/components/albumGallery/AlbumGallery.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useReducer } from 'react' +import AlbumTitle from '../album/AlbumTitle' +import MediaGallery, { + MEDIA_GALLERY_FRAGMENT, +} from '../photoGallery/MediaGallery' +import AlbumBoxes from './AlbumBoxes' +import AlbumFilter from '../album/AlbumFilter' +import { + mediaGalleryReducer, + urlPresentModeSetupHook, +} from '../photoGallery/mediaGalleryReducer' +import { MediaOrdering, SetOrderingFn } from '../../hooks/useOrderingParams' +import { gql } from '@apollo/client' +import { AlbumGalleryFields } from './__generated__/AlbumGalleryFields' + +export const ALBUM_GALLERY_FRAGMENT = gql` + ${MEDIA_GALLERY_FRAGMENT} + + fragment AlbumGalleryFields on Album { + id + title + subAlbums(order: { order_by: "title", order_direction: $orderDirection }) { + id + title + thumbnail { + id + thumbnail { + url + } + } + } + media( + paginate: { limit: $limit, offset: $offset } + order: { order_by: $mediaOrderBy, order_direction: $orderDirection } + onlyFavorites: $onlyFavorites + ) { + ...MediaGalleryFields + } + } +` + +type AlbumGalleryProps = { + album?: AlbumGalleryFields + loading?: boolean + customAlbumLink?(albumID: string): string + showFilter?: boolean + setOnlyFavorites?(favorites: boolean): void + setOrdering?: SetOrderingFn + ordering?: MediaOrdering + onlyFavorites?: boolean + onFavorite?(): void +} + +const AlbumGallery = React.forwardRef( + ( + { + album, + loading = false, + customAlbumLink, + showFilter = false, + setOnlyFavorites, + setOrdering, + ordering, + onlyFavorites = false, + }: AlbumGalleryProps, + ref: React.ForwardedRef + ) => { + const [mediaState, dispatchMedia] = useReducer(mediaGalleryReducer, { + presenting: false, + activeIndex: -1, + media: album?.media || [], + }) + + useEffect(() => { + dispatchMedia({ type: 'replaceMedia', media: album?.media || [] }) + }, [album?.media]) + +// urlPresentModeSetupHook({ +// dispatchMedia, +// openPresentMode: event => { +// dispatchMedia({ +// type: 'openPresentMode', +// activeIndex: event.state.activeIndex, +// }) +// }, +// }) + + let subAlbumElement = null + if (album) { + if (album.subAlbums.length > 0) { + subAlbumElement = ( + + ) + } + } else { + subAlbumElement = + } + + return ( +
    + {showFilter && ( + + )} + + {subAlbumElement} + +
    + ) + } +) + +export default AlbumGallery diff --git a/ui/src/components/albumGallery/__generated__/AlbumGalleryFields.ts b/ui/src/components/albumGallery/__generated__/AlbumGalleryFields.ts new file mode 100644 index 0000000..410c0b9 --- /dev/null +++ b/ui/src/components/albumGallery/__generated__/AlbumGalleryFields.ts @@ -0,0 +1,106 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL fragment: AlbumGalleryFields +// ==================================================== + +export interface AlbumGalleryFields_subAlbums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface AlbumGalleryFields_subAlbums_thumbnail { + __typename: "Media"; + id: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: AlbumGalleryFields_subAlbums_thumbnail_thumbnail | null; +} + +export interface AlbumGalleryFields_subAlbums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: AlbumGalleryFields_subAlbums_thumbnail | null; +} + +export interface AlbumGalleryFields_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface AlbumGalleryFields_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface AlbumGalleryFields_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface AlbumGalleryFields_media { + __typename: "Media"; + id: string; + type: MediaType; + /** + * A short string that can be used to generate a blured version of the media, to show while the original is loading + */ + blurhash: string | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: AlbumGalleryFields_media_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: AlbumGalleryFields_media_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: AlbumGalleryFields_media_videoWeb | null; + favorite: boolean; +} + +export interface AlbumGalleryFields { + __typename: "Album"; + id: string; + title: string; + /** + * The albums contained in this album + */ + subAlbums: AlbumGalleryFields_subAlbums[]; + /** + * The media inside this album + */ + media: AlbumGalleryFields_media[]; +} diff --git a/ui/src/components/facesOverlay/FacesOverlay.tsx b/ui/src/components/facesOverlay/FacesOverlay.tsx new file mode 100644 index 0000000..d60066e --- /dev/null +++ b/ui/src/components/facesOverlay/FacesOverlay.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import styled from 'styled-components' +import { MediaType } from '../../__generated__/globalTypes' +import { MediaSidebarMedia } from '../sidebar/MediaSidebar/MediaSidebar' +import { sidebarMediaQuery_media_faces } from '../sidebar/MediaSidebar/__generated__/sidebarMediaQuery' + +interface FaceBoxStyleProps { + $minY: number + $maxY: number + $minX: number + $maxX: number +} + +const FaceBoxStyle = styled(Link)` + box-shadow: inset 0 0 2px 1px rgba(0, 0, 0, 0.3), 0 0 0 1px rgb(255, 255, 255); + border-radius: 50%; + position: absolute; + top: ${({ $minY }: FaceBoxStyleProps) => $minY * 100}%; + bottom: ${({ $maxY }: FaceBoxStyleProps) => (1 - $maxY) * 100}%; + left: ${({ $minX }: FaceBoxStyleProps) => $minX * 100}%; + right: ${({ $maxX }: FaceBoxStyleProps) => (1 - $maxX) * 100}%; +` + +type FaceBoxProps = { + face: sidebarMediaQuery_media_faces +} + +const FaceBox = ({ face /*media*/ }: FaceBoxProps) => { + return ( + + ) +} + +const SidebarFacesOverlayWrapper = styled.div<{ width: number }>` + position: absolute; + width: ${({ width }) => width * 100}%; + left: ${({ width }) => (100 - width * 100) / 2}%; + height: 100%; + top: 0; + opacity: 0; + + user-select: none; + transition: opacity ease 200ms; + + &:hover { + opacity: 1; + } +` + +type SidebarFaceOverlayProps = { + media: MediaSidebarMedia +} + +export const SidebarFacesOverlay = ({ media }: SidebarFaceOverlayProps) => { + if (media.type != MediaType.Photo) return null + if (media.thumbnail == null) return null + + const faceBoxes = media.faces?.map(face => ( + + )) + + let wrapperWidth = 1 + if (media.thumbnail.width * 0.75 < media.thumbnail.height) { + wrapperWidth = (media.thumbnail.width * 0.75) / media.thumbnail.height + } + + return ( + + {faceBoxes} + + ) +} diff --git a/ui/src/components/header/Header.tsx b/ui/src/components/header/Header.tsx new file mode 100644 index 0000000..e10cb01 --- /dev/null +++ b/ui/src/components/header/Header.tsx @@ -0,0 +1,31 @@ +import React, { useContext } from 'react' +import SearchBar from './Searchbar' + +import { authToken } from '../../helpers/authentication' +import { SidebarContext } from '../sidebar/Sidebar' +import classNames from 'classnames' + +const Header = () => { + const { pinned } = useContext(SidebarContext) + + return ( +
    +

    + logo +

    + {authToken() ? : null} +
    + ) +} + +export default Header diff --git a/ui/src/components/header/Searchbar.tsx b/ui/src/components/header/Searchbar.tsx new file mode 100644 index 0000000..0df8e09 --- /dev/null +++ b/ui/src/components/header/Searchbar.tsx @@ -0,0 +1,402 @@ +import React, { useState, useRef, useEffect } from 'react' +import styled from 'styled-components' +import { useLazyQuery, gql } from '@apollo/client' +import { debounce, DebouncedFn } from '../../helpers/utils' +import { ProtectedImage } from '../photoGallery/ProtectedMedia' +import { NavLink, useNavigate, useLocation } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { + searchQuery, + searchQuery_search_albums, + searchQuery_search_media, +} from './__generated__/searchQuery' +import classNames from 'classnames' + +const SEARCH_QUERY = gql` + query searchQuery($query: String!) { + search(query: $query) { + query + albums { + id + title + thumbnail { + thumbnail { + url + } + } + } + media { + id + title + thumbnail { + url + } + album { + id + } + } + } + } +` + +const SearchWrapper = styled.div.attrs({ + className: 'w-full max-w-xs lg:relative', +})`` + +const SearchBar = () => { + const { t } = useTranslation() + const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY) + const [query, setQuery] = useState('') + const [fetched, setFetched] = useState(false) + const [expanded, setExpanded] = useState(false) + const inputEl = useRef(null) + + type QueryFn = (query: string) => void + + const debouncedFetch = useRef>(null) + useEffect(() => { + debouncedFetch.current = debounce(query => { + fetchSearches({ variables: { query } }) + setFetched(true) + setExpanded(true) + }, 250) + + return () => { + debouncedFetch.current?.cancel() + } + }, []) + + const fetchEvent = (e: React.ChangeEvent) => { + e.persist() + + setQuery(e.target.value) + if (e.target.value.trim() != '' && debouncedFetch.current) { + debouncedFetch.current(e.target.value.trim()) + } else { + setFetched(false) + } + } + + const location = useLocation() + useEffect(() => { + setExpanded(false) + setQuery('') + }, [location]) + + const [selectedItem, setSelectedItem] = useState(null) + + const searchData = fetchResult.data + let media = searchData?.search.media || [] + let albums = searchData?.search.albums || [] + + albums = albums.slice(0, 5) + media = media.slice(0, 5) + + const selectedItemId = + selectedItem !== null + ? [...albums.map(x => x.id), ...media.map(x => x.id)][selectedItem] + : null + + useEffect(() => { + const elem = inputEl.current + if (!elem) return + + const focusEvent = () => { + setExpanded(true) + } + + const blurEvent = () => { + setExpanded(false) + } + + elem.addEventListener('focus', focusEvent) + elem.addEventListener('blur', blurEvent) + + return () => { + elem.removeEventListener('focus', focusEvent) + elem.removeEventListener('blur', blurEvent) + } + }, [inputEl]) + + useEffect(() => { + setSelectedItem(null) + }, [searchData]) + + useEffect(() => { + const totalItems = albums.length + media.length + + const keydownEvent = (event: KeyboardEvent) => { + if (!expanded) return + + if (event.key == 'ArrowDown') { + event.preventDefault() + setSelectedItem(i => (i === null ? 0 : Math.min(totalItems - 1, i + 1))) + } else if (event.key == 'ArrowUp') { + event.preventDefault() + setSelectedItem(i => (i === null ? 0 : Math.max(0, i - 1))) + } else if (event.key == 'Escape') { + // setExpanded(false) + inputEl.current?.blur() + } + } + + document.addEventListener('keydown', keydownEvent) + + return () => { + document.removeEventListener('keydown', keydownEvent) + } + }, [searchData]) + + let results = null + if (query.trim().length > 0 && fetched) { + results = ( + + ) + } + + return ( + + + {results} + + ) +} + +const ResultTitle = styled.h1.attrs({ + className: + 'uppercase text-gray-700 dark:text-gray-200 text-sm font-semibold mt-4 mb-2 mx-1', +})`` + +type SearchResultsProps = { + albums: searchQuery_search_albums[] + media: searchQuery_search_media[] + loading: boolean + selectedItem: number | null + setSelectedItem: React.Dispatch> + query: string + expanded: boolean +} + +const SearchResults = ({ + albums, + media, + loading, + selectedItem, + setSelectedItem, + query, + expanded, +}: SearchResultsProps) => { + const { t } = useTranslation() + + const albumElements = albums.map((album, i) => ( + setSelectedItem(i)} + /> + )) + + const mediaElements = media.map((media, i) => ( + setSelectedItem(i + albumElements.length)} + /> + )) + + let message = null + if (loading) message = t('header.search.loading', 'Loading results...') + else if (media.length == 0 && albums.length == 0) + message = t('header.search.no_results', 'No results found') + + if (message) message =
    {message}
    + + return ( +
    { + // Prevent input blur event + e.preventDefault() + }} + > + {message} + {albumElements.length > 0 && ( + <> + + {t('header.search.result_type.albums', 'Albums')} + +
      {albumElements}
    + + )} + {mediaElements.length > 0 && ( + <> + + {t('header.search.result_type.media', 'Media')} + +
      {mediaElements}
    + + )} +
    + ) +} + +type SearchRowProps = { + id: string + link: string + preview: React.ReactNode + label: React.ReactNode + selected: boolean + setSelected(): void +} + +const SearchRow = ({ + id, + link, + preview, + label, + selected, + setSelected, +}: SearchRowProps) => { + const rowEl = useRef(null) + const navigate = useNavigate() + + useEffect(() => { + const keydownEvent = (event: KeyboardEvent) => { + if (event.key == 'Enter') navigate(link) + } + + document.addEventListener('keydown', keydownEvent) + + return () => { + document.removeEventListener('keydown', keydownEvent) + } + }) + + if (selected) { + rowEl.current?.scrollIntoView({ + block: 'nearest', + }) + } + + return ( +
  • setSelected()} + className={classNames('rounded p-1 mt-1', { + 'bg-gray-100 dark:bg-dark-bg2': selected, + })} + > + + {preview} + {label} + +
  • + ) +} + +type PhotoRowArgs = { + query: string + media: searchQuery_search_media + selected: boolean + setSelected(): void +} + +const PhotoRow = ({ query, media, selected, setSelected }: PhotoRowArgs) => ( + + } + label={searchHighlighted(query, media.title)} + selected={selected} + setSelected={setSelected} + /> +) + +type AlbumRowArgs = { + query: string + album: searchQuery_search_albums + selected: boolean + setSelected(): void +} + +const AlbumRow = ({ query, album, selected, setSelected }: AlbumRowArgs) => ( + + } + label={searchHighlighted(query, album.title)} + selected={selected} + setSelected={setSelected} + /> +) + +const searchHighlighted = (query: string, text: string) => { + const i = text.toLowerCase().indexOf(query.toLowerCase()) + + if (i == -1) { + return text + } + + const start = text.substring(0, i) + const middle = text.substring(i, i + query.length) + const end = text.substring(i + query.length) + + return ( + + {start} + {middle} + {end} + + ) +} + +export default SearchBar diff --git a/ui/src/components/header/__generated__/searchQuery.ts b/ui/src/components/header/__generated__/searchQuery.ts new file mode 100644 index 0000000..cf49659 --- /dev/null +++ b/ui/src/components/header/__generated__/searchQuery.ts @@ -0,0 +1,88 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: searchQuery +// ==================================================== + +export interface searchQuery_search_albums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface searchQuery_search_albums_thumbnail { + __typename: "Media"; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: searchQuery_search_albums_thumbnail_thumbnail | null; +} + +export interface searchQuery_search_albums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: searchQuery_search_albums_thumbnail | null; +} + +export interface searchQuery_search_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface searchQuery_search_media_album { + __typename: "Album"; + id: string; +} + +export interface searchQuery_search_media { + __typename: "Media"; + id: string; + title: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: searchQuery_search_media_thumbnail | null; + /** + * The album that holds the media + */ + album: searchQuery_search_media_album; +} + +export interface searchQuery_search { + __typename: "SearchResult"; + /** + * The string that was searched for + */ + query: string; + /** + * A list of albums that matched the query + */ + albums: searchQuery_search_albums[]; + /** + * A list of media that matched the query + */ + media: searchQuery_search_media[]; +} + +export interface searchQuery { + /** + * Perform a search query on the contents of the media library + */ + search: searchQuery_search; +} + +export interface searchQueryVariables { + query: string; +} diff --git a/ui/src/components/layout/Layout.test.tsx b/ui/src/components/layout/Layout.test.tsx new file mode 100644 index 0000000..ddbe318 --- /dev/null +++ b/ui/src/components/layout/Layout.test.tsx @@ -0,0 +1,14 @@ +import { render, screen } from '@testing-library/react' +import React from 'react' +import Layout from './Layout' + +test('Layout component', () => { + render( + +

    layout_content

    +
    + ) + + expect(screen.getByTestId('Layout')).toBeInTheDocument() + expect(screen.getByText('layout_content')).toBeInTheDocument() +}) diff --git a/ui/src/components/layout/Layout.tsx b/ui/src/components/layout/Layout.tsx new file mode 100644 index 0000000..1f075f9 --- /dev/null +++ b/ui/src/components/layout/Layout.tsx @@ -0,0 +1,53 @@ +import { gql } from '@apollo/client' +import React, { useContext } from 'react' +import { Helmet } from 'react-helmet' +import Header from '../header/Header' +import { Authorized } from '../routes/AuthorizedRoute' +import { Sidebar, SidebarContext } from '../sidebar/Sidebar' +import MainMenu from './MainMenu' + +export const ADMIN_QUERY = gql` + query adminQuery { + myUser { + admin + } + } +` + +type LayoutProps = { + children: React.ReactNode + title: string +} + +const Layout = ({ children, title, ...otherProps }: LayoutProps) => { + const { pinned, content: sidebarContent } = useContext(SidebarContext) + + return ( + <> + + + {title ? `${title} - Gallery.FuKun.Net` : `Gallery.FuKun.Net`} + + +
    +
    +
    + + + +
    + {children} +
    +
    + +
    + + ) +} + +export default Layout diff --git a/ui/src/components/layout/MainMenu.test.tsx b/ui/src/components/layout/MainMenu.test.tsx new file mode 100644 index 0000000..94cece3 --- /dev/null +++ b/ui/src/components/layout/MainMenu.test.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import { MockedProvider } from '@apollo/client/testing' +import { render, screen } from '@testing-library/react' + +import * as authentication from '../../helpers/authentication' +import { ADMIN_QUERY } from './Layout' +import { MemoryRouter } from 'react-router-dom' +import MainMenu, { MAPBOX_QUERY } from './MainMenu' + +vi.mock('../../helpers/authentication.ts') + +const authTokenMock = vi.mocked(authentication.authToken) + +afterEach(() => { + authTokenMock.mockClear() +}) + +test('Layout sidebar component', async () => { + authTokenMock.mockImplementation(() => 'test-token') + + const mockedGraphql = [ + { + request: { + query: ADMIN_QUERY, + }, + result: { + data: { + myUser: { + admin: true, + }, + }, + }, + }, + { + request: { + query: MAPBOX_QUERY, + }, + result: { + data: { + mapboxToken: true, + }, + }, + }, + ] + + render( + + + + + + ) + + expect(screen.getByText('Timeline')).toBeInTheDocument() + expect(screen.getByText('Albums')).toBeInTheDocument() + + expect(await screen.findByText('Settings')).toBeInTheDocument() + expect(await screen.findByText('Places')).toBeInTheDocument() +}) diff --git a/ui/src/components/layout/MainMenu.tsx b/ui/src/components/layout/MainMenu.tsx new file mode 100644 index 0000000..e4662a0 --- /dev/null +++ b/ui/src/components/layout/MainMenu.tsx @@ -0,0 +1,159 @@ +import React from 'react' +import { NavLink } from 'react-router-dom' +import { useQuery, gql } from '@apollo/client' +import { authToken } from '../../helpers/authentication' +import { useTranslation } from 'react-i18next' +import { mapboxEnabledQuery } from '../../__generated__/mapboxEnabledQuery' +import { tailwindClassNames } from '../../helpers/utils' +import { faceDetectionEnabled } from './__generated__/faceDetectionEnabled' + +export const MAPBOX_QUERY = gql` + query mapboxEnabledQuery { + mapboxToken + } +` + +export const FACE_DETECTION_ENABLED_QUERY = gql` + query faceDetectionEnabled { + siteInfo { + faceDetectionEnabled + } + } +` + +type MenuButtonProps = { + to: string + exact: boolean + label: string + background: string + activeClasses?: string + className?: string + icon?: React.ReactNode +} + +const MenuButton = ({ + to, + exact, + label, + background, + icon, + activeClasses, + className, +}: MenuButtonProps) => { + return ( + + tailwindClassNames( + `rounded-lg my-2 outline-none`, + className, + { + activeClasses: isActive, + } + ) + } + > +
  • +
    + {icon} +
    + {label} +
  • +
    + ) +} + +const MenuSeparator = () => ( +
    +) + +export const MainMenu = () => { + const { t } = useTranslation() + + const mapboxQuery = authToken() + ? useQuery(MAPBOX_QUERY) + : null + const faceDetectionEnabledQuery = authToken() + ? useQuery(FACE_DETECTION_ENABLED_QUERY) + : null + + const mapboxEnabled = !!mapboxQuery?.data?.mapboxToken + const faceDetectionEnabled = + !!faceDetectionEnabledQuery?.data?.siteInfo?.faceDetectionEnabled + + return ( +
    +
      + + + + + + } + /> + + + + } + /> + + + + + } + /> +
    +
    + ) +} + +export default MainMenu diff --git a/ui/src/components/layout/__generated__/adminQuery.ts b/ui/src/components/layout/__generated__/adminQuery.ts new file mode 100644 index 0000000..a150b03 --- /dev/null +++ b/ui/src/components/layout/__generated__/adminQuery.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: adminQuery +// ==================================================== + +export interface adminQuery_myUser { + __typename: "User"; + /** + * Whether or not the user has admin privileges + */ + admin: boolean; +} + +export interface adminQuery { + /** + * Information about the currently logged in user + */ + myUser: adminQuery_myUser; +} diff --git a/ui/src/components/layout/__generated__/faceDetectionEnabled.ts b/ui/src/components/layout/__generated__/faceDetectionEnabled.ts new file mode 100644 index 0000000..4afd838 --- /dev/null +++ b/ui/src/components/layout/__generated__/faceDetectionEnabled.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: faceDetectionEnabled +// ==================================================== + +export interface faceDetectionEnabled_siteInfo { + __typename: "SiteInfo"; + /** + * Whether or not face detection is enabled and working + */ + faceDetectionEnabled: boolean; +} + +export interface faceDetectionEnabled { + siteInfo: faceDetectionEnabled_siteInfo; +} diff --git a/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts b/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts new file mode 100644 index 0000000..e666de4 --- /dev/null +++ b/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mapboxEnabledQuery +// ==================================================== + +export interface mapboxEnabledQuery { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null; +} diff --git a/ui/src/components/layout/menu-icons/albums.svg b/ui/src/components/layout/menu-icons/albums.svg new file mode 100644 index 0000000..1e93f5e --- /dev/null +++ b/ui/src/components/layout/menu-icons/albums.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/people.svg b/ui/src/components/layout/menu-icons/people.svg new file mode 100644 index 0000000..2af9060 --- /dev/null +++ b/ui/src/components/layout/menu-icons/people.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/photos.svg b/ui/src/components/layout/menu-icons/photos.svg new file mode 100644 index 0000000..5b801f1 --- /dev/null +++ b/ui/src/components/layout/menu-icons/photos.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/places.svg b/ui/src/components/layout/menu-icons/places.svg new file mode 100644 index 0000000..3032ea3 --- /dev/null +++ b/ui/src/components/layout/menu-icons/places.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/settings.svg b/ui/src/components/layout/menu-icons/settings.svg new file mode 100644 index 0000000..709a765 --- /dev/null +++ b/ui/src/components/layout/menu-icons/settings.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/mapbox/MapboxMap.tsx b/ui/src/components/mapbox/MapboxMap.tsx new file mode 100644 index 0000000..b2a3087 --- /dev/null +++ b/ui/src/components/mapbox/MapboxMap.tsx @@ -0,0 +1,83 @@ +import React, { useState, useRef, useEffect } from 'react' +import { gql, useQuery } from '@apollo/client' +import type mapboxgl from 'mapbox-gl' +import styled from 'styled-components' + +import 'mapbox-gl/dist/mapbox-gl.css' +import { mapboxToken } from './__generated__/mapboxToken' +import { isDarkMode } from '../../theme' + +const MAPBOX_TOKEN_QUERY = gql` + query mapboxToken { + mapboxToken + myMediaGeoJson + } +` + +const MapContainer = styled.div` + width: 100%; + height: 100%; +` + +type MapboxMapProps = { + configureMapbox(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl): void + mapboxOptions?: Partial +} + +const useMapboxMap = ({ + configureMapbox, + mapboxOptions = undefined, +}: MapboxMapProps) => { + const [mapboxLibrary, setMapboxLibrary] = useState() + const mapContainer = useRef(null) + const map = useRef(null) + + const { data: mapboxData } = useQuery(MAPBOX_TOKEN_QUERY, { + fetchPolicy: 'cache-first', + }) + + useEffect(() => { + async function loadMapboxLibrary() { + const mapbox = (await import('mapbox-gl')).default + + setMapboxLibrary(mapbox) + } + loadMapboxLibrary() + }, []) + + useEffect(() => { + if ( + mapboxLibrary == null || + mapContainer.current == null || + mapboxData == null || + map.current != null + ) { + return + } + + if (mapboxData.mapboxToken) + mapboxLibrary.accessToken = mapboxData.mapboxToken + + map.current = new mapboxLibrary.Map({ + container: mapContainer.current, + style: isDarkMode() + ? 'mapbox://styles/mapbox/dark-v10' + : 'mapbox://styles/mapbox/streets-v11', + ...mapboxOptions, + }) + + configureMapbox(map.current, mapboxLibrary) + map.current?.resize() + }, [mapContainer, mapboxLibrary, mapboxData]) + + map.current?.resize() + + return { + mapContainer: , + mapboxMap: map.current, + mapboxLibrary, + mapboxToken: mapboxData?.mapboxToken || null, + } +} + +export default useMapboxMap diff --git a/ui/src/components/mapbox/__generated__/mapboxToken.ts b/ui/src/components/mapbox/__generated__/mapboxToken.ts new file mode 100644 index 0000000..e3e01ce --- /dev/null +++ b/ui/src/components/mapbox/__generated__/mapboxToken.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mapboxToken +// ==================================================== + +export interface mapboxToken { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null; + /** + * Get media owned by the logged in user, returned in GeoJson format + */ + myMediaGeoJson: Any; +} diff --git a/ui/src/components/mapbox/mapboxHelperFunctions.tsx b/ui/src/components/mapbox/mapboxHelperFunctions.tsx new file mode 100644 index 0000000..6ea3a20 --- /dev/null +++ b/ui/src/components/mapbox/mapboxHelperFunctions.tsx @@ -0,0 +1,93 @@ +import type mapboxgl from 'mapbox-gl' +import type geojson from 'geojson' +import React from 'react' +import ReactDOM from 'react-dom' +import MapClusterMarker from '../../Pages/PlacesPage/MapClusterMarker' +import { MediaMarker } from '../../Pages/PlacesPage/MapPresentMarker' +import { PlacesAction } from '../../Pages/PlacesPage/placesReducer' + +const markers: { [key: string]: mapboxgl.Marker } = {} +let markersOnScreen: typeof markers = {} + +type registerMediaMarkersArgs = { + map: mapboxgl.Map + mapboxLibrary: typeof mapboxgl + dispatchMarkerMedia: React.Dispatch +} + +/** + * Add appropriate event handlers to the map, to render and update media markers + * Expects the provided mapbox map to contain geojson source of media + */ +export const registerMediaMarkers = (args: registerMediaMarkersArgs) => { + const updateMarkers = makeUpdateMarkers(args) + + args.map.on('move', updateMarkers) + args.map.on('moveend', updateMarkers) + args.map.on('sourcedata', updateMarkers) + updateMarkers() +} + +/** + * Make a function that can be passed to Mapbox to tell it how to render and update the image markers + */ +const makeUpdateMarkers = + ({ map, mapboxLibrary, dispatchMarkerMedia }: registerMediaMarkersArgs) => + () => { + const newMarkers: typeof markers = {} + const features = map.querySourceFeatures('media') + + // for every media on the screen, create an HTML marker for it (if we didn't yet), + // and add it to the map if it's not there already + for (const feature of features) { + const point = feature.geometry as geojson.Point + const coords = point.coordinates as [number, number] + const props = feature.properties as MediaMarker + if (props == null) { + console.warn('WARN: geojson feature had no properties', feature) + continue + } + + const id = props.cluster + ? `cluster_${props.cluster_id}` + : `media_${props.media_id}` + + let marker = markers[id] + if (!marker) { + const el = createClusterPopupElement(props, { + dispatchMarkerMedia, + }) + marker = markers[id] = new mapboxLibrary.Marker({ + element: el, + }).setLngLat(coords) + } + newMarkers[id] = marker + + if (!markersOnScreen[id]) marker.addTo(map) + } + // for every marker we've added previously, remove those that are no longer visible + for (const id in markersOnScreen) { + if (!newMarkers[id]) markersOnScreen[id].remove() + } + markersOnScreen = newMarkers + } + +function createClusterPopupElement( + geojsonProps: MediaMarker, + { + dispatchMarkerMedia, + }: { + dispatchMarkerMedia: React.Dispatch + } +) { + // setPresentMarker: React.Dispatch> + const el = document.createElement('div') + ReactDOM.render( + , + el + ) + return el +} diff --git a/ui/src/components/messages/Message.tsx b/ui/src/components/messages/Message.tsx new file mode 100644 index 0000000..a79440e --- /dev/null +++ b/ui/src/components/messages/Message.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { forwardRef } from 'react' +import { ReactComponent as DismissIcon } from './icons/dismissIcon.svg' + +export type MessageProps = { + header: string + content?: string + children?: React.ReactNode + onDismiss?(): void +} + +const Message = forwardRef( + ( + { onDismiss, header, children, content }: MessageProps, + ref: React.ForwardedRef + ) => { + return ( +
    + +

    {header}

    +
    {content}
    + {children} +
    + ) + } +) + +export default Message diff --git a/ui/src/components/messages/MessageProgress.tsx b/ui/src/components/messages/MessageProgress.tsx new file mode 100644 index 0000000..1215ec2 --- /dev/null +++ b/ui/src/components/messages/MessageProgress.tsx @@ -0,0 +1,30 @@ +import React, { forwardRef } from 'react' +import MessagePlain, { MessageProps } from './Message' + +type MessageProgressProps = MessageProps & { + percent?: number +} + +const MessageProgress = forwardRef( + ( + { header, content, percent = 0, ...props }: MessageProgressProps, + ref: React.ForwardedRef + ) => { + let color = '#dc2625' + if (percent > 33) color = '#fbbf24' + if (percent > 66) color = '#56e263' + + return ( + +
    +
    +
    +
    + ) + } +) + +export default MessageProgress diff --git a/ui/src/components/messages/Messages.tsx b/ui/src/components/messages/Messages.tsx new file mode 100644 index 0000000..d4827e3 --- /dev/null +++ b/ui/src/components/messages/Messages.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { authToken } from '../../helpers/authentication' +import MessageProgress from './MessageProgress' +import MessagePlain from './Message' +import SubscriptionsHook, { Message } from './SubscriptionsHook' +import { NotificationType } from '../../__generated__/globalTypes' + +const Container = styled.div` + position: fixed; + bottom: 20px; + right: 20px; + width: 500px; + + @media (max-width: 1000px) { + display: none; + } +` + +type MessageStateType = { + set: React.Dispatch> + get: Message[] + add(message: Message): void + removeKey(key: string): void +} + +export const MessageState: MessageStateType = { + set: fn => { + console.warn('set function is not defined yet, called with', fn) + }, + get: [], + add: message => { + MessageState.set(messages => { + const newMessages = messages.filter(msg => msg.key != message.key) + newMessages.push(message) + + return newMessages + }) + }, + removeKey: key => { + MessageState.set(messages => { + const newMessages = messages.filter(msg => msg.key != key) + return newMessages + }) + }, +} + +const Messages = () => { + const [messages, setMessages] = useState([]) + MessageState.set = setMessages + MessageState.get = messages + + const getMessageElement = (message: Message): React.FunctionComponent => { + const dismissMessage = (message: Message) => { + message.onDismiss && message.onDismiss() + setMessages(messages => messages.filter(msg => msg.key != message.key)) + } + + switch (message.type) { + case NotificationType.Message: + return props => ( + { + dismissMessage(message) + }} + {...message.props} + {...props} + /> + ) + case NotificationType.Progress: + return props => ( + { + dismissMessage(message) + }} + {...message.props} + {...props} + /> + ) + default: + throw new Error(`Invalid message type: ${message.type}`) + } + } + + // const transitions = useTransition(messages.slice().reverse(), x => x.key, { + // from: { + // opacity: 0, + // height: '0px', + // }, + // enter: { + // opacity: 1, + // height: `100px`, + // }, + // leave: { opacity: 0, height: '0px' }, + // }) + + const messageElems = messages.map(msg => { + const Elem = getMessageElement(msg) + return ( +
    + +
    + ) + }) + + return ( + + {messageElems} + {authToken() && ( + + )} + + ) +} + +export default Messages diff --git a/ui/src/components/messages/SubscriptionsHook.ts b/ui/src/components/messages/SubscriptionsHook.ts new file mode 100644 index 0000000..49c720d --- /dev/null +++ b/ui/src/components/messages/SubscriptionsHook.ts @@ -0,0 +1,124 @@ +import { notificationSubscription } from './__generated__/notificationSubscription' +import { useEffect } from 'react' +import { useSubscription, gql } from '@apollo/client' +import { authToken } from '../../helpers/authentication' +import { NotificationType } from '../../__generated__/globalTypes' + +const NOTIFICATION_SUBSCRIPTION = gql` + subscription notificationSubscription { + notification { + key + type + header + content + progress + positive + negative + timeout + } + } +` + +const messageTimeoutHandles = new Map() + +export interface Message { + key: string + type: NotificationType + timeout?: number + onDismiss?: () => void + props: { + header: string + content: string + negative?: boolean + positive?: boolean + percent?: number + } +} + +type SubscriptionHookProps = { + messages: Message[] + setMessages: React.Dispatch> +} + +const SubscriptionsHook = ({ + messages, + setMessages, +}: SubscriptionHookProps) => { + if (!authToken()) { + return null + } + + const { data, error } = useSubscription( + NOTIFICATION_SUBSCRIPTION + ) + + useEffect(() => { + if (error) { + setMessages(state => [ + ...state, + { + key: Math.random().toString(26), + type: NotificationType.Message, + props: { + header: 'Network error', + content: error.message, + negative: true, + }, + }, + ]) + } + + if (!data) return + + const newMessages = [...messages] + + const msg = data.notification + + if (msg.type == 'Close') { + setMessages(messages => messages.filter(m => m.key != msg.key)) + return + } + + const newNotification: Message = { + key: msg.key, + type: msg.type, + timeout: msg.timeout || undefined, + props: { + header: msg.header, + content: msg.content, + negative: msg.negative, + positive: msg.positive, + percent: msg.progress || undefined, + }, + } + + if (msg.timeout) { + // Clear old timeout, to replace it with the new one + if (messageTimeoutHandles.get(msg.key)) { + const timeoutHandle = messageTimeoutHandles.get(msg.key) + clearTimeout(timeoutHandle) + } + + const timeoutHandle = setTimeout(() => { + setMessages(messages => messages.filter(m => m.key != msg.key)) + }, msg.timeout) as unknown as number + + messageTimeoutHandles.set(msg.key, timeoutHandle) + } + + const notifyIndex = newMessages.findIndex( + msg => msg.key == newNotification.key + ) + if (notifyIndex != -1) { + newMessages[notifyIndex] = newNotification + } else { + newMessages.push(newNotification) + } + + setMessages(newMessages) + }, [data, error]) + + return null +} + +export default SubscriptionsHook diff --git a/ui/src/components/messages/__generated__/notificationSubscription.ts b/ui/src/components/messages/__generated__/notificationSubscription.ts new file mode 100644 index 0000000..399b7db --- /dev/null +++ b/ui/src/components/messages/__generated__/notificationSubscription.ts @@ -0,0 +1,47 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { NotificationType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL subscription operation: notificationSubscription +// ==================================================== + +export interface notificationSubscription_notification { + __typename: "Notification"; + /** + * A key used to identify the notification, new notification updates with the same key, should replace the old notifications + */ + key: string; + type: NotificationType; + /** + * The text for the title of the notification + */ + header: string; + /** + * The text for the body of the notification + */ + content: string; + /** + * A value between 0 and 1 when the notification type is `Progress` + */ + progress: number | null; + /** + * Whether or not the message of the notification is positive, the UI might reflect this with a green color + */ + positive: boolean; + /** + * Whether or not the message of the notification is negative, the UI might reflect this with a red color + */ + negative: boolean; + /** + * Time in milliseconds before the notification should close + */ + timeout: number | null; +} + +export interface notificationSubscription { + notification: notificationSubscription_notification; +} diff --git a/ui/src/components/messages/icons/dismissIcon.svg b/ui/src/components/messages/icons/dismissIcon.svg new file mode 100644 index 0000000..cc121dc --- /dev/null +++ b/ui/src/components/messages/icons/dismissIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/components/photoGallery/MediaGallery.test.tsx b/ui/src/components/photoGallery/MediaGallery.test.tsx new file mode 100644 index 0000000..529b025 --- /dev/null +++ b/ui/src/components/photoGallery/MediaGallery.test.tsx @@ -0,0 +1,124 @@ +import { render, screen } from '@testing-library/react' + +import React from 'react' +import { MediaType } from '../../__generated__/globalTypes' +import MediaGallery from './MediaGallery' +import { MediaGalleryState } from './mediaGalleryReducer' + +vi.mock('./photoGalleryMutations', () => ({ + useMarkFavoriteMutation: () => [vi.fn()], +})) + +test('photo gallery with media', () => { + const dispatchMedia = vi.fn() + + const mediaState: MediaGalleryState = { + activeIndex: 0, + media: [ + { + id: '165', + type: MediaType.Photo, + thumbnail: { + url: '/photo/thumbnail_3666760020_jpg_x76GG5pS.jpg', + width: 768, + height: 1024, + __typename: 'MediaURL', + }, + highRes: null, + videoWeb: null, + blurhash: null, + favorite: false, + __typename: 'Media', + }, + { + id: '122', + type: MediaType.Photo, + thumbnail: null, + highRes: null, + videoWeb: null, + blurhash: null, + favorite: false, + __typename: 'Media', + }, + { + id: '98', + type: MediaType.Video, + thumbnail: null, + highRes: null, + videoWeb: null, + blurhash: null, + favorite: false, + __typename: 'Media', + }, + ], + presenting: false, + } + + render( + + ) + + expect( + screen.getByTestId('photo-gallery-wrapper').querySelectorAll('img') + ).toHaveLength(3) +}) + +describe('photo gallery presenting', () => { + const dispatchMedia = vi.fn() + + test('not presenting', () => { + const mediaStateNoPresent: MediaGalleryState = { + activeIndex: -1, + media: [], + presenting: false, + } + + render( + + ) + + expect(screen.queryByTestId('present-overlay')).not.toBeInTheDocument() + }) + + test('presenting', () => { + const mediaStatePresent: MediaGalleryState = { + activeIndex: 0, + media: [ + { + id: '165', + type: MediaType.Photo, + thumbnail: { + url: '/photo/thumbnail_3666760020_jpg_x76GG5pS.jpg', + width: 768, + height: 1024, + __typename: 'MediaURL', + }, + highRes: null, + videoWeb: null, + blurhash: null, + favorite: false, + __typename: 'Media', + }, + ], + presenting: true, + } + + render( + + ) + + expect(screen.getByTestId('present-overlay')).toBeInTheDocument() + }) +}) diff --git a/ui/src/components/photoGallery/MediaGallery.tsx b/ui/src/components/photoGallery/MediaGallery.tsx new file mode 100644 index 0000000..f9cc83a --- /dev/null +++ b/ui/src/components/photoGallery/MediaGallery.tsx @@ -0,0 +1,126 @@ +import React, { useContext } from 'react' +import styled from 'styled-components' +import { MediaThumbnail, MediaPlaceholder } from './MediaThumbnail' +import PresentView from './presentView/PresentView' +import { + openPresentModeAction, + PhotoGalleryAction, + MediaGalleryState, +} from './mediaGalleryReducer' +import { + toggleFavoriteAction, + useMarkFavoriteMutation, +} from './photoGalleryMutations' +import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar' +import { SidebarContext } from '../sidebar/Sidebar' +import { gql } from '@apollo/client' + +const Gallery = styled.div` + display: flex; + flex-wrap: wrap; + align-items: center; + min-height: 200px; + position: relative; + margin: -4px; + + @media (max-width: 1000px) { + /* Compensate for tab bar on mobile */ + margin-bottom: 76px; + } +` + +export const PhotoFiller = styled.div` + height: 200px; + flex-grow: 999999; +` + +export const MEDIA_GALLERY_FRAGMENT = gql` + fragment MediaGalleryFields on Media { + id + type + blurhash + thumbnail { + url + width + height + } + highRes { + url + } + videoWeb { + url + } + favorite + } +` + +type MediaGalleryProps = { + loading: boolean + mediaState: MediaGalleryState + dispatchMedia: React.Dispatch +} + +const MediaGallery = ({ mediaState, dispatchMedia }: MediaGalleryProps) => { + const [markFavorite] = useMarkFavoriteMutation() + + const { media, activeIndex, presenting } = mediaState + + const { updateSidebar } = useContext(SidebarContext) + + let mediaElements = [] + if (media) { + mediaElements = media.map((media, index) => { + const active = activeIndex == index + + return ( + { + dispatchMedia({ + type: 'selectImage', + index, + }) + updateSidebar() + }} + clickFavorite={() => { + toggleFavoriteAction({ + media, + markFavorite, + }) + }} + clickPresent={() => { + dispatchMedia({ + type: 'selectImage', + index, + }) + updateSidebar() + }} + /> + ) + }) + } else { + for (let i = 0; i < 6; i++) { + mediaElements.push() + } + } + + return ( + <> + + {mediaElements} + + + {/* PresentView disabled — click opens sidebar instead */} + {/* presenting && ( + + ) */} + + ) +} + +export default MediaGallery diff --git a/ui/src/components/photoGallery/MediaThumbnail.tsx b/ui/src/components/photoGallery/MediaThumbnail.tsx new file mode 100644 index 0000000..7034965 --- /dev/null +++ b/ui/src/components/photoGallery/MediaThumbnail.tsx @@ -0,0 +1,220 @@ +import React from 'react' +import styled from 'styled-components' +import { ProtectedImage } from './ProtectedMedia' +import { MediaType } from '../../__generated__/globalTypes' +import { ReactComponent as VideoThumbnailIconSVG } from './icons/videoThumbnailIcon.svg' +import { MediaGalleryFields } from './__generated__/MediaGalleryFields' + +const MediaContainer = styled.div` + flex-grow: 1; + flex-basis: 0; + height: 200px; + margin: 4px 2px; + background-color: #eee; + position: relative; + overflow: hidden; +` + +const StyledPhoto = styled(ProtectedImage)` + height: 200px; + min-width: 100%; + position: relative; + object-fit: cover; + + transition: opacity 300ms; +` + +type LazyPhotoProps = { + src?: string + blurhash: string | null +} + +const LazyPhoto = (photoProps: LazyPhotoProps) => { + return +} + +const PhotoOverlay = styled.div<{ active: boolean }>` + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + + ${({ active }) => + active && + ` + outline: 4px solid rgba(65, 131, 196, 0.6); + outline-offset: -4px; + `} +` + +const HoverIcon = styled.button` + font-size: 1.5em; + margin: 160px 10px 0 10px; + color: white; + text-shadow: 0 0 4px black; + opacity: 0; + position: relative; + + border-radius: 50%; + width: 34px; + height: 34px; + + ${MediaContainer}:hover &, ${MediaContainer}:focus-within & { + opacity: 1 !important; + } + + &:hover, + &:focus { + background-color: rgba(0, 0, 0, 0.4); + } + + transition: opacity 100ms, background-color 100ms; +` + +type FavoriteIconProps = { + favorite: boolean + onClick(e: React.MouseEvent): void +} + +const FavoriteIcon = ({ favorite, onClick }: FavoriteIconProps) => { + return ( + + + + + + ) +} + +type SidebarIconProps = { + onClick(e: React.MouseEvent): void +} + +const SidebarIcon = ({ onClick }: SidebarIconProps) => ( + + + + + +) + +const SidebarIconWrapper = styled(HoverIcon)` + margin: 10px !important; + position: absolute; + top: 0; + right: 0; +` + +const VideoThumbnailIcon = styled(VideoThumbnailIconSVG)` + color: rgba(255, 255, 255, 0.8); + position: absolute; + left: calc(50% - 17.5px); + top: calc(50% - 22px); +` + +type MediaThumbnailProps = { + media: MediaGalleryFields + active: boolean + selectImage(): void + clickPresent(): void + clickFavorite(): void +} + +export const MediaThumbnail = ({ + media, + active, + selectImage, + clickPresent, + clickFavorite, +}: MediaThumbnailProps) => { + let heartIcon = null + if (media.favorite !== undefined) { + heartIcon = ( + { + e.stopPropagation() + clickFavorite() + }} + /> + ) + } + + let videoIcon = null + if (media.type == MediaType.Video) { + videoIcon = + } + + let minWidth = 100 + if (media.thumbnail) { + minWidth = Math.floor( + (media.thumbnail.width / media.thumbnail.height) * 200 + ) + } + + return ( + { + clickPresent() + }} + > +
    + +
    + + {videoIcon} + { + e.stopPropagation() + selectImage() + }} + /> + {heartIcon} + +
    + ) +} + +export const MediaPlaceholder = styled.div` + flex-grow: 1; + height: 200px; + width: 300px; + margin: 4px 2px; + background-color: #eee; + position: relative; +` diff --git a/ui/src/components/photoGallery/ProtectedMedia.tsx b/ui/src/components/photoGallery/ProtectedMedia.tsx new file mode 100644 index 0000000..6ed9887 --- /dev/null +++ b/ui/src/components/photoGallery/ProtectedMedia.tsx @@ -0,0 +1,201 @@ +import classNames from 'classnames' +import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react' +import { useRef } from 'react' +import { useState } from 'react' +import { useEffect } from 'react' +import { BlurhashCanvas } from 'react-blurhash' +import { isNil } from '../../helpers/utils' + +const isNativeLazyLoadSupported = 'loading' in document.createElement('img') +const placeholder = + 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' + +const getProtectedUrl = (url?: string) => { + if (url == undefined) return undefined + + const imgUrl = new URL(url, location.origin) + + const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/) + if (tokenRegex) { + const token = tokenRegex[1] + imgUrl.searchParams.set('token', token) + } + + return imgUrl.href +} + +export interface ProtectedImageProps extends Omit< + DetailedHTMLProps, HTMLImageElement>, + 'src' +> { + src?: string + key?: string + lazyLoading?: boolean + blurhash?: string | null +} + +/** + * An image that needs authorization to load + * Set lazyLoading to true if you want the image to be loaded once it enters the viewport + * Native lazy load via HTMLImageElement.loading attribute will be preferred if it is supported by the browser, + * otherwise IntersectionObserver will be used. + */ +export const ProtectedImage = ({ + src, + lazyLoading, + blurhash, + ...props +}: ProtectedImageProps) => { + const [loaded, setLoaded] = useState(false) + + const url = getProtectedUrl(src) || placeholder + + const didLoad = () => setLoaded(true) + + if (!lazyLoading) { + return ( + + ) + } + + if (!isNativeLazyLoadSupported) { + return + } + + // load with native lazy loading + return ( +
    + + {blurhash && !loaded && ( + + )} +
    + ) +} + +interface FallbackLazyloadedImageProps extends Omit< + DetailedHTMLProps, HTMLImageElement>, + 'src' +> { + src?: string + blurhash?: string | null +} + +const FallbackLazyloadedImage = ({ + src, + blurhash, + className, + ...props +}: FallbackLazyloadedImageProps) => { + const [inView, setInView] = useState(false) + const [loaded, setLoaded] = useState(false) + + const imgRef = useRef(null) + + const didLoad = () => setLoaded(true) + + useEffect(() => { + const imgElm = imgRef.current + if (isNil(imgElm) || inView) return + + if (window.IntersectionObserver === undefined) { + setInView(true) + return + } + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setInView(true) + observer.disconnect() + } + }, + { + root: null, + threshold: 0, + } + ) + + observer.observe(imgElm) + + return () => { + observer.disconnect() + } + }, [imgRef]) + + if (inView) { + return ( +
    + + {blurhash && !loaded && ( + + )} +
    + ) + } else { + return ( +
    + {blurhash && ( + + )} +
    + ) + } +} + +export interface ProtectedVideoProps_Media { + __typename: 'Media' + id: string + thumbnail?: null | { + __typename: 'MediaURL' + url: string + } + videoWeb?: null | { + __typename: 'MediaURL' + url: string + } +} + +export interface ProtectedVideoProps { + media: ProtectedVideoProps_Media +} + +export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => { + if (isNil(media.videoWeb)) { + console.error('ProetctedVideo called with media.videoWeb = null') + return null + } + + return ( + + ) +} diff --git a/ui/src/components/photoGallery/__generated__/MediaGalleryFields.ts b/ui/src/components/photoGallery/__generated__/MediaGalleryFields.ts new file mode 100644 index 0000000..03b7c74 --- /dev/null +++ b/ui/src/components/photoGallery/__generated__/MediaGalleryFields.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL fragment: MediaGalleryFields +// ==================================================== + +export interface MediaGalleryFields_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface MediaGalleryFields_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface MediaGalleryFields_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface MediaGalleryFields { + __typename: "Media"; + id: string; + type: MediaType; + /** + * A short string that can be used to generate a blured version of the media, to show while the original is loading + */ + blurhash: string | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: MediaGalleryFields_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: MediaGalleryFields_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: MediaGalleryFields_videoWeb | null; + favorite: boolean; +} diff --git a/ui/src/components/photoGallery/__generated__/markMediaFavorite.ts b/ui/src/components/photoGallery/__generated__/markMediaFavorite.ts new file mode 100644 index 0000000..c5ddb94 --- /dev/null +++ b/ui/src/components/photoGallery/__generated__/markMediaFavorite.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: markMediaFavorite +// ==================================================== + +export interface markMediaFavorite_favoriteMedia { + __typename: "Media"; + id: string; + favorite: boolean; +} + +export interface markMediaFavorite { + /** + * Mark or unmark a media as being a favorite + */ + favoriteMedia: markMediaFavorite_favoriteMedia; +} + +export interface markMediaFavoriteVariables { + mediaId: string; + favorite: boolean; +} diff --git a/ui/src/components/photoGallery/icons/videoThumbnailIcon.svg b/ui/src/components/photoGallery/icons/videoThumbnailIcon.svg new file mode 100644 index 0000000..ee6b4b5 --- /dev/null +++ b/ui/src/components/photoGallery/icons/videoThumbnailIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/photoGallery/mediaGalleryReducer.tsx b/ui/src/components/photoGallery/mediaGalleryReducer.tsx new file mode 100644 index 0000000..f506b71 --- /dev/null +++ b/ui/src/components/photoGallery/mediaGalleryReducer.tsx @@ -0,0 +1,127 @@ +import React, { useEffect } from 'react' +import { MediaGalleryFields } from './__generated__/MediaGalleryFields' + +export interface MediaGalleryState { + presenting: boolean + activeIndex: number + media: MediaGalleryFields[] +} + +export type GalleryAction = + | { type: 'nextImage' } + | { type: 'previousImage' } + | { type: 'closePresentMode' } + +export type PhotoGalleryAction = + | GalleryAction + | { type: 'openPresentMode'; activeIndex: number } + | { type: 'selectImage'; index: number } + | { type: 'replaceMedia'; media: MediaGalleryFields[] } + +export function mediaGalleryReducer( + state: MediaGalleryState, + action: PhotoGalleryAction +): MediaGalleryState { + switch (action.type) { + case 'nextImage': + return { + ...state, + activeIndex: (state.activeIndex + 1) % state.media.length, + } + case 'previousImage': + if (state.activeIndex <= 0) { + return { + ...state, + activeIndex: state.media.length - 1, + } + } else { + return { + ...state, + activeIndex: state.activeIndex - 1, + } + } + case 'openPresentMode': + return { + ...state, + presenting: true, + activeIndex: action.activeIndex, + } + case 'closePresentMode': + return { + ...state, + presenting: false, + } + case 'selectImage': + return { + ...state, + activeIndex: Math.max( + 0, + Math.min(state.media.length - 1, action.index) + ), + } + case 'replaceMedia': + return { + ...state, + media: action.media, + activeIndex: -1, + presenting: false, + } + } +} + +export interface MediaGalleryPopStateEvent extends PopStateEvent { + state: MediaGalleryState +} + +export const urlPresentModeSetupHook = ({ + dispatchMedia, + openPresentMode, +}: { + dispatchMedia: React.Dispatch + openPresentMode: (event: MediaGalleryPopStateEvent) => void +}) => { + useEffect(() => { + const urlChangeListener = (event: MediaGalleryPopStateEvent) => { + if (event.state.presenting === true) { + openPresentMode(event) + } else { + dispatchMedia({ type: 'closePresentMode' }) + } + } + + window.addEventListener('popstate', urlChangeListener) + + history.replaceState({ presenting: false }, '') + + return () => { + window.removeEventListener('popstate', urlChangeListener) + } + }, []) +} + +export const openPresentModeAction = ({ + dispatchMedia, + activeIndex, +}: { + dispatchMedia: React.Dispatch + activeIndex: number +}) => { + dispatchMedia({ + type: 'openPresentMode', + activeIndex: activeIndex, + }) + + history.pushState({ presenting: true, activeIndex }, '') +} + +export const closePresentModeAction = ({ + dispatchMedia, +}: { + dispatchMedia: React.Dispatch +}) => { + dispatchMedia({ + type: 'closePresentMode', + }) + + history.back() +} diff --git a/ui/src/components/photoGallery/photoGalleryMutations.ts b/ui/src/components/photoGallery/photoGalleryMutations.ts new file mode 100644 index 0000000..25b1ec8 --- /dev/null +++ b/ui/src/components/photoGallery/photoGalleryMutations.ts @@ -0,0 +1,43 @@ +import { MediaGalleryFields } from './__generated__/MediaGalleryFields' +import { gql, MutationFunction, useMutation } from '@apollo/client' +import { + markMediaFavorite, + markMediaFavoriteVariables, +} from './__generated__/markMediaFavorite' + +const markFavoriteMutation = gql` + mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) { + favoriteMedia(mediaId: $mediaId, favorite: $favorite) { + id + favorite + } + } +` + +export const useMarkFavoriteMutation = () => { + return useMutation( + markFavoriteMutation + ) +} + +export const toggleFavoriteAction = ({ + media, + markFavorite, +}: { + media: MediaGalleryFields + markFavorite: MutationFunction +}) => { + return markFavorite({ + variables: { + mediaId: media.id, + favorite: !media.favorite, + }, + optimisticResponse: { + favoriteMedia: { + id: media.id, + favorite: !media.favorite, + __typename: 'Media', + }, + }, + }) +} diff --git a/ui/src/components/photoGallery/photoGalleryReducer.test.ts b/ui/src/components/photoGallery/photoGalleryReducer.test.ts new file mode 100644 index 0000000..d7f857d --- /dev/null +++ b/ui/src/components/photoGallery/photoGalleryReducer.test.ts @@ -0,0 +1,113 @@ +import { mediaGalleryReducer, MediaGalleryState } from './mediaGalleryReducer' +import { MediaType } from '../../__generated__/globalTypes' + +describe('photo gallery reducer', () => { + const defaultState: MediaGalleryState = { + presenting: false, + activeIndex: 0, + media: [ + { + __typename: 'Media', + id: '1', + highRes: null, + thumbnail: null, + blurhash: null, + type: MediaType.Photo, + }, + { + __typename: 'Media', + id: '2', + highRes: null, + thumbnail: null, + blurhash: null, + type: MediaType.Photo, + }, + { + __typename: 'Media', + id: '3', + highRes: null, + thumbnail: null, + blurhash: null, + type: MediaType.Photo, + }, + ], + } + + test('next image', () => { + expect(mediaGalleryReducer(defaultState, { type: 'nextImage' })).toEqual({ + ...defaultState, + activeIndex: 1, + }) + + expect( + mediaGalleryReducer( + { ...defaultState, activeIndex: 2 }, + { type: 'nextImage' } + ) + ).toEqual({ + ...defaultState, + activeIndex: 0, + }) + }) + + test('previous image', () => { + expect( + mediaGalleryReducer(defaultState, { type: 'previousImage' }) + ).toEqual({ + ...defaultState, + activeIndex: 2, + }) + + expect( + mediaGalleryReducer( + { ...defaultState, activeIndex: 2 }, + { type: 'previousImage' } + ) + ).toEqual({ + ...defaultState, + activeIndex: 1, + }) + }) + + test('select image', () => { + expect( + mediaGalleryReducer(defaultState, { type: 'selectImage', index: 1 }) + ).toEqual({ + ...defaultState, + activeIndex: 1, + }) + + expect( + mediaGalleryReducer(defaultState, { type: 'selectImage', index: 100 }) + ).toEqual({ + ...defaultState, + activeIndex: 2, + }) + + expect( + mediaGalleryReducer(defaultState, { type: 'selectImage', index: -5 }) + ).toEqual({ + ...defaultState, + activeIndex: 0, + }) + }) + + test('present mode', () => { + const openState = mediaGalleryReducer(defaultState, { + type: 'openPresentMode', + activeIndex: 10, + }) + expect(openState).toEqual({ + ...defaultState, + presenting: true, + activeIndex: 10, + }) + + expect( + mediaGalleryReducer(openState, { type: 'closePresentMode' }) + ).toEqual({ + ...openState, + presenting: false, + }) + }) +}) diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.test.tsx b/ui/src/components/photoGallery/presentView/PresentMedia.test.tsx new file mode 100644 index 0000000..09ffeb4 --- /dev/null +++ b/ui/src/components/photoGallery/presentView/PresentMedia.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react' + +import React from 'react' +import { MediaType } from '../../../__generated__/globalTypes' +import { MediaGalleryFields } from '../__generated__/MediaGalleryFields' +import PresentMedia from './PresentMedia' + +test('render present image', () => { + const media: MediaGalleryFields = { + __typename: 'Media', + id: '123', + type: MediaType.Photo, + highRes: null, + blurhash: null, + videoWeb: null, + favorite: false, + thumbnail: { + __typename: 'MediaURL', + url: '/sample_image.jpg', + width: 300, + height: 200, + }, + } + + render() + + expect(screen.getByTestId('present-img-thumbnail')).toHaveAttribute( + 'src', + 'http://localhost:3000/sample_image.jpg' + ) + expect(screen.getByTestId('present-img-highres')).toHaveStyle({ + display: 'none', + }) +}) + +test('render present video', () => { + const media: MediaGalleryFields = { + __typename: 'Media', + id: '123', + type: MediaType.Video, + highRes: null, + blurhash: null, + favorite: false, + videoWeb: { + __typename: 'MediaURL', + url: '/sample_video.mp4', + }, + thumbnail: { + __typename: 'MediaURL', + url: '/sample_video_thumb.jpg', + width: 300, + height: 200, + }, + } + + render() + + expect(screen.getByTestId('present-video')).toHaveAttribute( + 'poster', + 'http://localhost:3000/sample_video_thumb.jpg' + ) + + expect( + screen.getByTestId('present-video').querySelector('source') + ).toHaveAttribute('src', 'http://localhost:3000/sample_video.mp4') +}) diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.tsx b/ui/src/components/photoGallery/presentView/PresentMedia.tsx new file mode 100644 index 0000000..7aa5eb4 --- /dev/null +++ b/ui/src/components/photoGallery/presentView/PresentMedia.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import styled from 'styled-components' +import { MediaType } from '../../../__generated__/globalTypes' +import { exhaustiveCheck } from '../../../helpers/utils' +import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia' +import { MediaGalleryFields } from '../__generated__/MediaGalleryFields' + +const StyledPhoto = styled(ProtectedImage)` + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + object-fit: contain; + object-position: center; +` + +const StyledVideo = styled(ProtectedVideo)` + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; +` + +type PresentMediaProps = { + media: MediaGalleryFields + imageLoaded?(): void +} + +const PresentMedia = ({ + media, + imageLoaded, + ...otherProps +}: PresentMediaProps) => { + switch (media.type) { + case MediaType.Photo: + return ( +
    + + { + const elem = e.target as HTMLImageElement + elem.style.display = 'initial' + imageLoaded && imageLoaded() + }} + /> +
    + ) + case MediaType.Video: + return + } + + exhaustiveCheck(media.type) +} + +export default PresentMedia diff --git a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.test.tsx b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.test.tsx new file mode 100644 index 0000000..4a6893a --- /dev/null +++ b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.test.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import PresentNavigationOverlay from './PresentNavigationOverlay' +import { fireEvent, render, screen, act } from '@testing-library/react' + +vi.useFakeTimers() + +describe('PresentNavigationOverlay component', () => { + test('simple render', () => { + const dispatchMedia = vi.fn() + render() + + expect(screen.getByLabelText('Previous image')).toBeInTheDocument() + expect(screen.getByLabelText('Next image')).toBeInTheDocument() + expect(screen.getByLabelText('Exit presentation mode')).toBeInTheDocument() + }) + + test('click buttons', () => { + const dispatchMedia = vi.fn() + render() + + expect(dispatchMedia).not.toHaveBeenCalled() + + fireEvent.click(screen.getByLabelText('Next image')) + expect(dispatchMedia).lastCalledWith({ type: 'nextImage' }) + + fireEvent.click(screen.getByLabelText('Previous image')) + expect(dispatchMedia).lastCalledWith({ type: 'previousImage' }) + }) + + test('mouse move, show and hide', () => { + const dispatchMedia = vi.fn() + const { container } = render( + + ) + + expect(screen.getByLabelText('Next image')).toHaveClass('hide') + + fireEvent.mouseMove(container.firstChild!) + expect(screen.getByLabelText('Next image')).not.toHaveClass('hide') + + act(() => { + vi.advanceTimersByTime(3000) + }) + + expect(screen.getByLabelText('Next image')).toHaveClass('hide') + }) +}) diff --git a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx new file mode 100644 index 0000000..3962cde --- /dev/null +++ b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx @@ -0,0 +1,148 @@ +import React, { useState, useRef, useEffect } from 'react' +import styled from 'styled-components' +import { debounce, DebouncedFn } from '../../../helpers/utils' +import { closePresentModeAction, GalleryAction } from '../mediaGalleryReducer' + +import { useSwipeable } from 'react-swipeable' + +import ExitIcon from './icons/Exit' +import NextIcon from './icons/Next' +import PrevIcon from './icons/Previous' + +const StyledOverlayContainer = styled.div` + width: 100%; + height: 100%; + position: relative; +` + +const OverlayButton = styled.button` + width: 64px; + height: 64px; + background: none; + border: none; + outline: none; + cursor: pointer; + position: absolute; + + & svg { + width: 32px; + height: 32px; + overflow: visible !important; + } + + & svg path { + stroke: rgba(255, 255, 255, 0.5); + transition-property: stroke, filter; + transition-duration: 140ms; + } + + &:hover svg path { + stroke: rgba(255, 255, 255, 1); + filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.6)); + } + + &.hide svg path { + stroke: rgba(255, 255, 255, 0); + transition: stroke 300ms; + } +` + +const ExitButton = styled(OverlayButton)` + left: 28px; + top: 28px; +` + +const NavigationButton = styled(OverlayButton)<{ align: 'left' | 'right' }>` + height: 80%; + width: 20%; + top: 10%; + + ${({ align: float }) => (float == 'left' ? 'left: 0;' : null)} + ${({ align: float }) => (float == 'right' ? 'right: 0;' : null)} + + & svg { + margin: auto; + width: 48px; + height: 64px; + } +` + +type PresentNavigationOverlayProps = { + children?: React.ReactChild + dispatchMedia: React.Dispatch + disableSaveCloseInHistory?: boolean +} + +const PresentNavigationOverlay = ({ + children, + dispatchMedia, + disableSaveCloseInHistory, +}: PresentNavigationOverlayProps) => { + const [hide, setHide] = useState(true) + const onMouseMove = useRef void>>(null) + + useEffect(() => { + onMouseMove.current = debounce( + () => { + setHide(hide => !hide) + }, + 2000, + true + ) + + return () => { + onMouseMove.current?.cancel() + } + }, []) + + const handlers = useSwipeable({ + onSwipedLeft: () => dispatchMedia({ type: 'nextImage' }), + onSwipedRight: () => dispatchMedia({ type: 'previousImage' }), + preventScrollOnSwipe: false, + trackMouse: false, + }) + + return ( + { + onMouseMove.current && onMouseMove.current() + }} + > +
    + {children} + dispatchMedia({ type: 'previousImage' })} + > + + + dispatchMedia({ type: 'nextImage' })} + > + + + { + if (disableSaveCloseInHistory === true) { + dispatchMedia({ type: 'closePresentMode' }) + } else { + closePresentModeAction({ dispatchMedia }) + } + }} + > + + +
    +
    + ) +} + +export default PresentNavigationOverlay diff --git a/ui/src/components/photoGallery/presentView/PresentView.tsx b/ui/src/components/photoGallery/presentView/PresentView.tsx new file mode 100644 index 0000000..d022d2c --- /dev/null +++ b/ui/src/components/photoGallery/presentView/PresentView.tsx @@ -0,0 +1,83 @@ +import React, { useEffect } from 'react' +import styled, { createGlobalStyle } from 'styled-components' +import PresentNavigationOverlay from './PresentNavigationOverlay' +import PresentMedia from './PresentMedia' +import { closePresentModeAction, GalleryAction } from '../mediaGalleryReducer' +import { MediaGalleryFields } from '../__generated__/MediaGalleryFields' + +const StyledContainer = styled.div` + position: fixed; + width: 100vw; + height: 100vh; + background-color: black; + color: white; + top: 0; + left: 0; + z-index: 100; +` + +const PreventScroll = createGlobalStyle` + * { + overflow: hidden !important; + } +` + +type PresentViewProps = { + className?: string + imageLoaded?(): void + activeMedia: MediaGalleryFields + dispatchMedia: React.Dispatch + disableSaveCloseInHistory?: boolean +} + +const PresentView = ({ + className, + imageLoaded, + activeMedia, + dispatchMedia, + disableSaveCloseInHistory, +}: PresentViewProps) => { + useEffect(() => { + const keyDownEvent = (e: KeyboardEvent) => { + if (e.key == 'ArrowRight') { + e.stopPropagation() + dispatchMedia({ type: 'nextImage' }) + } + + if (e.key == 'ArrowLeft') { + e.stopPropagation() + dispatchMedia({ type: 'previousImage' }) + } + + if (e.key == 'Escape') { + e.stopPropagation() + + if (disableSaveCloseInHistory === true) { + dispatchMedia({ type: 'closePresentMode' }) + } else { + closePresentModeAction({ dispatchMedia }) + } + } + } + + document.addEventListener('keydown', keyDownEvent) + + return function cleanup() { + document.removeEventListener('keydown', keyDownEvent) + } + }) + + return ( + + + + + + + ) +} + +export default PresentView diff --git a/ui/src/components/photoGallery/presentView/icons/Exit.tsx b/ui/src/components/photoGallery/presentView/icons/Exit.tsx new file mode 100644 index 0000000..c26952f --- /dev/null +++ b/ui/src/components/photoGallery/presentView/icons/Exit.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +function SvgExit(props: React.SVGProps) { + return ( + + + + + + ) +} + +export default SvgExit diff --git a/ui/src/components/photoGallery/presentView/icons/Next.tsx b/ui/src/components/photoGallery/presentView/icons/Next.tsx new file mode 100644 index 0000000..5389ac3 --- /dev/null +++ b/ui/src/components/photoGallery/presentView/icons/Next.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +function SvgNext(props: React.SVGProps) { + return ( + + + + ) +} + +export default SvgNext diff --git a/ui/src/components/photoGallery/presentView/icons/Previous.tsx b/ui/src/components/photoGallery/presentView/icons/Previous.tsx new file mode 100644 index 0000000..73f4fdb --- /dev/null +++ b/ui/src/components/photoGallery/presentView/icons/Previous.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +function SvgPrevious(props: React.SVGProps) { + return ( + + + + ) +} + +export default SvgPrevious diff --git a/ui/src/components/routes/AuthorizedRoute.test.tsx b/ui/src/components/routes/AuthorizedRoute.test.tsx new file mode 100644 index 0000000..8b3503d --- /dev/null +++ b/ui/src/components/routes/AuthorizedRoute.test.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import AuthorizedRoute, { useIsAdmin } from './AuthorizedRoute' +import { render, screen, waitFor } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' + +import * as authentication from '../../helpers/authentication' +import { MockedProvider } from '@apollo/client/testing' +import { ADMIN_QUERY } from '../layout/Layout' + +vi.mock('../../helpers/authentication.ts') + +const authToken = vi.mocked(authentication.authToken) + +describe('AuthorizedRoute component', () => { + const AuthorizedComponent = () =>
    authorized content
    + + test('not logged in', () => { + authToken.mockImplementation(() => null) + + render( + + + redirect} /> + + + + } + /> + + + ) + + expect(screen.getByText('redirect')).toBeInTheDocument() + expect(screen.queryByText('authorized content')).toBeNull() + }) + + test('logged in', () => { + authToken.mockImplementation(() => 'token-here') + + render( + + + redirect} /> + + + + } + /> + + + ) + + expect(screen.getByText('authorized content')).toBeInTheDocument() + expect(screen.queryByText('redirect')).toBeNull() + }) +}) + +describe('useIsAdmin hook', () => { + const graphqlMock = (admin: boolean) => ({ + request: { + query: ADMIN_QUERY, + }, + result: { + data: { + myUser: { + admin, + }, + }, + }, + }) + + test('not logged in', async () => { + authToken.mockImplementation(() => null) + + const TestComponent = () => { + const isAdmin = useIsAdmin() + return isAdmin ?
    is admin
    :
    not admin
    + } + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('not admin')).toBeInTheDocument() + }) + }) + + test('not admin', async () => { + authToken.mockImplementation(() => 'token-here') + + const TestComponent = () => { + const isAdmin = useIsAdmin() + return isAdmin ?
    is admin
    :
    not admin
    + } + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('not admin')).toBeInTheDocument() + }) + }) + + test('is admin', async () => { + authToken.mockImplementation(() => 'token-here') + + const TestComponent = () => { + const isAdmin = useIsAdmin() + return isAdmin ?
    is admin
    :
    not admin
    + } + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('is admin')).toBeInTheDocument() + }) + }) +}) diff --git a/ui/src/components/routes/AuthorizedRoute.tsx b/ui/src/components/routes/AuthorizedRoute.tsx new file mode 100644 index 0000000..825ca49 --- /dev/null +++ b/ui/src/components/routes/AuthorizedRoute.tsx @@ -0,0 +1,45 @@ +import { useLazyQuery } from '@apollo/client' +import React, { useEffect } from 'react' +import { Navigate } from 'react-router-dom' +import { authToken } from '../../helpers/authentication' +import { adminQuery } from '../../__generated__/adminQuery' +import { ADMIN_QUERY } from '../layout/Layout' + +export const useIsAdmin = () => { + const [fetchAdminQuery, { data, called }] = + useLazyQuery(ADMIN_QUERY) + + useEffect(() => { + if (authToken() && !called) { + fetchAdminQuery() + } + }, [authToken()]) + + if (!authToken()) { + return false + } + + return data?.myUser?.admin +} + +export const Authorized = ({ children }: { children: JSX.Element }) => { + const token = authToken() + + return token ? children : null +} + +interface AuthorizedRouteProps { + children: React.ReactNode +} + +const AuthorizedRoute = ({ children }: AuthorizedRouteProps) => { + const token = authToken() + + if (!token) { + return + } + + return <>{children} +} + +export default AuthorizedRoute diff --git a/ui/src/components/routes/Routes.test.tsx b/ui/src/components/routes/Routes.test.tsx new file mode 100644 index 0000000..9b8e0e5 --- /dev/null +++ b/ui/src/components/routes/Routes.test.tsx @@ -0,0 +1,41 @@ +import React from 'react' + +import Routes from './Routes' +import { + render, + screen, + waitForElementToBeRemoved, +} from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' + +vi.mock('../../Pages/LoginPage/LoginPage.tsx', () => () => ( +
    mocked login page
    +)) + +describe('routes', () => { + // vitest does not support this yet + // https://github.com/vitest-dev/vitest/issues/960 + test.skip('unauthorized root path should navigate to login page', async () => { + render( + + + + ) + + await waitForElementToBeRemoved(() => + screen.getByText('Loading', { exact: false }) + ) + + expect(screen.getByText('mocked login page')).toBeInTheDocument() + }) + + test('invalid page should print a "not found" message', () => { + render( + + + + ) + + expect(screen.getByText('Page not found')).toBeInTheDocument() + }) +}) diff --git a/ui/src/components/routes/Routes.tsx b/ui/src/components/routes/Routes.tsx new file mode 100644 index 0000000..a4e550a --- /dev/null +++ b/ui/src/components/routes/Routes.tsx @@ -0,0 +1,157 @@ +import React from 'react' +import { + Navigate, + useNavigate, + NavigateFunction, + Outlet, + useRoutes, +} from 'react-router-dom' + +import Layout from '../layout/Layout' +import { authToken, clearTokenCookie } from '../../helpers/authentication' +import { TFunction, useTranslation } from 'react-i18next' +import Loader from '../../primitives/Loader' +import AuthorizedRoute from './AuthorizedRoute' + +const AlbumsPage = React.lazy( + () => import('../../Pages/AllAlbumsPage/AlbumsPage') +) +const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage')) +const TimelinePage = React.lazy( + () => import('../../Pages/TimelinePage/TimelinePage') +) +const PlacesPage = React.lazy(() => import('../../Pages/PlacesPage/PlacesPage')) + +const LoginPage = React.lazy(() => import('../../Pages/LoginPage/LoginPage')) +const InitialSetupPage = React.lazy( + () => import('../../Pages/LoginPage/InitialSetupPage') +) + +const SharePageTokenRoute = React.lazy(() => + import('../../Pages/SharePage/SharePage').then(x => ({ + default: x.TokenRoute, + })) +) + +const SettingsPage = React.lazy( + () => import('../../Pages/SettingsPage/SettingsPage') +) + +const PeoplePage = React.lazy(() => + import('../../Pages/PeoplePage/PeoplePage').then(x => ({ + default: x.PeoplePage, + })) +) + +const PersonPage = React.lazy(() => + import('../../Pages/PeoplePage/PeoplePage').then(x => ({ + default: x.PersonPage, + })) +) + +const Routes = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + const authorized = (element: React.ReactNode) => ( + {element} + ) + + const routes = useRoutes([ + { + index: true, + element: , + }, + { + path: '/login', + element: , + }, + { + path: '/logout', + element: , + }, + { + path: '/initialSetup', + element: , + }, + { + path: '/share/:token/*', + element: , + }, + { + path: '/albums', + element: authorized(), + }, + { + path: '/album/:id', + element: authorized(), + }, + { + path: '/timeline', + element: authorized(), + }, + { + path: '/places', + element: authorized(), + }, + { + path: '/settings', + element: authorized(), + }, + { + path: '/people', + element: authorized(), + children: [ + { + path: ':person', + element: , + }, + { + index: true, + element: , + }, + ], + }, + { + // for backwards-compatibility + path: '/photos', + element: , + }, + { + path: '*', + element: , + }, + ]) + + return ( + + + + } + > + {routes} + + ) +} + +const IndexPage = () => { + const token = authToken() + + const dest = token ? '/timeline' : '/login' + + return +} + +export const NotFoundPage = ({ t }: { t: TFunction }) => { + return
    {t('routes.page_not_found', 'Page not found')}
    +} + +const LogoutPage = ({ navigate }: { navigate: NavigateFunction }) => { + clearTokenCookie() + navigate('/') + return null +} + +export default Routes diff --git a/ui/src/components/sidebar/AlbumCovers.tsx b/ui/src/components/sidebar/AlbumCovers.tsx new file mode 100644 index 0000000..f4c04aa --- /dev/null +++ b/ui/src/components/sidebar/AlbumCovers.tsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect } from 'react' +import { useMutation, gql } from '@apollo/client' +import { useTranslation } from 'react-i18next' + +import { SidebarSection, SidebarSectionTitle } from './SidebarComponents' + +import { + setAlbumCover, + setAlbumCoverVariables, +} from './__generated__/setAlbumCover' +import { + resetAlbumCover, + resetAlbumCoverVariables, +} from './__generated__/resetAlbumCover' +import { authToken } from '../../helpers/authentication' + +const RESET_ALBUM_COVER_MUTATION = gql` + mutation resetAlbumCover($albumID: ID!) { + resetAlbumCover(albumID: $albumID) { + id + thumbnail { + id + thumbnail { + url + } + } + } + } +` +const SET_ALBUM_COVER_MUTATION = gql` + mutation setAlbumCover($coverID: ID!) { + setAlbumCover(coverID: $coverID) { + id + thumbnail { + id + thumbnail { + url + } + } + } + } +` + +type SidebarPhotoCoverProps = { + cover_id: string +} + +export const SidebarPhotoCover = ({ cover_id }: SidebarPhotoCoverProps) => { + const { t } = useTranslation() + + const [setAlbumCover] = useMutation( + SET_ALBUM_COVER_MUTATION, + { + variables: { + coverID: cover_id, + }, + } + ) + + const [buttonDisabled, setButtonDisabled] = useState(false) + + useEffect(() => { + setButtonDisabled(false) + }, [cover_id]) + + // hide when not authenticated + if (!authToken()) { + return null + } + + return ( + + + {t('sidebar.album.album_cover', 'Album cover')} + +
    + + + + + + +
    + +
    +
    +
    + ) +} + +type SidebarAlbumCoverProps = { + id: string +} + +export const SidebarAlbumCover = ({ id }: SidebarAlbumCoverProps) => { + const { t } = useTranslation() + + const [resetAlbumCover] = useMutation< + resetAlbumCover, + resetAlbumCoverVariables + >(RESET_ALBUM_COVER_MUTATION, { + variables: { + albumID: id, + }, + }) + + const [buttonDisabled, setButtonDisabled] = useState(false) + + useEffect(() => { + setButtonDisabled(false) + }, [id]) + + return ( + + + {t('sidebar.album.album_cover', 'Album cover')} + +
    + + + + + + +
    + +
    +
    +
    + ) +} diff --git a/ui/src/components/sidebar/AlbumSidebar.tsx b/ui/src/components/sidebar/AlbumSidebar.tsx new file mode 100644 index 0000000..1707467 --- /dev/null +++ b/ui/src/components/sidebar/AlbumSidebar.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import { useQuery, gql } from '@apollo/client' +import { SidebarAlbumShare } from './Sharing' +import { useTranslation } from 'react-i18next' +import SidebarHeader from './SidebarHeader' +import { + getAlbumSidebar, + getAlbumSidebarVariables, +} from './__generated__/getAlbumSidebar' +import { SidebarAlbumCover } from './AlbumCovers' +import SidebarAlbumDownload from './SidebarDownloadAlbum' + +const albumQuery = gql` + query getAlbumSidebar($id: ID!) { + album(id: $id) { + id + title + } + } +` + +type AlbumSidebarProps = { + albumId: string +} + +const AlbumSidebar = ({ albumId }: AlbumSidebarProps) => { + const { t } = useTranslation() + const { loading, error, data } = useQuery< + getAlbumSidebar, + getAlbumSidebarVariables + >(albumQuery, { + variables: { id: albumId }, + }) + + if (loading) return
    {t('general.loading.default', 'Loading...')}
    + if (error) return
    {error.message}
    + + return ( +
    + {/*

    {t('sidebar.album.title', 'Album options')}

    */} + +
    + {/*

    {data.album.title}

    */} + +
    +
    + +
    +
    + +
    +
    + ) +} + +export default AlbumSidebar diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.tsx new file mode 100644 index 0000000..88bf446 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { MockedProvider } from '@apollo/client/testing' +import MediaSidebar, { MediaSidebarMedia } from './MediaSidebar' +import { MediaType } from '../../../__generated__/globalTypes' +import { MemoryRouter } from 'react-router' + +import * as authentication from '../../../helpers/authentication' + +vi.mock('../../../helpers/authentication.ts') + +const authToken = vi.mocked(authentication.authToken) + +describe('MediaSidebar', () => { + const media: MediaSidebarMedia = { + __typename: 'Media', + id: '6867', + title: '122A6069.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail.jpg', + width: 1024, + height: 839, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/highres.jpg', + width: 5322, + height: 4362, + }, + videoWeb: null, + album: { + __typename: 'Album', + id: '2294', + title: 'album_name', + }, + } + + test('render sample image, unauthorized', () => { + authToken.mockImplementation(() => null) + + render( + + + + + + ) + + expect(screen.getByText('122A6069.jpg')).toBeInTheDocument() + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + 'http://localhost:3000/photo/highres.jpg' + ) + + expect( + screen.queryByText('Set as album cover photo') + ).not.toBeInTheDocument() + expect(screen.queryByText('Sharing options')).not.toBeInTheDocument() + }) + + test('render sample image, authorized', () => { + authToken.mockImplementation(() => 'token-here') + + render( + + + + + + ) + + expect(screen.getByText('122A6069.jpg')).toBeInTheDocument() + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + 'http://localhost:3000/photo/highres.jpg' + ) + + expect(screen.getByText('Set as album cover photo')).toBeInTheDocument() + expect(screen.getByText('Album path')).toBeInTheDocument() + + screen.debug() + }) +}) diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx new file mode 100644 index 0000000..b1f1ef2 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx @@ -0,0 +1,308 @@ +import { gql, useLazyQuery } from '@apollo/client' +import React, { useEffect, useContext } from 'react' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import styled from 'styled-components' +import { authToken } from '../../../helpers/authentication' +import { isNil } from '../../../helpers/utils' +import { MediaType } from '../../../__generated__/globalTypes' +import { SidebarFacesOverlay } from '../../facesOverlay/FacesOverlay' +import { SidebarContext } from '../Sidebar' +import { + ProtectedImage, + ProtectedVideo, + ProtectedVideoProps_Media, +} from '../../photoGallery/ProtectedMedia' +import { SidebarPhotoCover } from '../AlbumCovers' +import { SidebarPhotoShare } from '../Sharing' +import SidebarMediaDownload from '../SidebarDownloadMedia' +import SidebarHeader from '../SidebarHeader' +import { sidebarDownloadQuery_media_downloads } from '../__generated__/sidebarDownloadQuery' +import ExifDetails from './MediaSidebarExif' +import MediaSidebarPeople from './MediaSidebarPeople' +import MediaSidebarMap from './MediaSidebarMap' +import { + sidebarMediaQuery, + sidebarMediaQueryVariables, + sidebarMediaQuery_media_album_path, + sidebarMediaQuery_media_exif, + sidebarMediaQuery_media_faces, + sidebarMediaQuery_media_thumbnail, + sidebarMediaQuery_media_videoMetadata, +} from './__generated__/sidebarMediaQuery' +import { BreadcrumbList } from '../../album/AlbumTitle' + +export const SIDEBAR_MEDIA_QUERY = gql` + query sidebarMediaQuery($id: ID!) { + media(id: $id) { + id + title + type + highRes { + url + width + height + } + thumbnail { + url + width + height + } + videoWeb { + url + width + height + } + videoMetadata { + id + width + height + duration + codec + framerate + bitrate + colorProfile + audio + } + exif { + id + description + camera + maker + lens + dateShot + exposure + aperture + iso + focalLength + flash + exposureProgram + coordinates { + latitude + longitude + } + } + album { + id + title + path { + id + title + } + } + faces { + id + rectangle { + minX + maxX + minY + maxY + } + faceGroup { + id + label + imageFaceCount + } + media { + id + title + thumbnail { + url + width + height + } + } + } + } + } +` + +const PreviewImage = styled(ProtectedImage)` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + object-fit: contain; +` + +const PreviewVideo = styled(ProtectedVideo)` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +` + +interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media { + type: MediaType +} + +type PreviewMediaProps = { + media: PreviewMediaPropsMedia + previewImage?: { + url: string + } +} + +const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => { + if (media.type === MediaType.Photo) { + return + } + + if (media.type === MediaType.Video) { + return + } + + return
    ERROR: Unknown media type: {media.type}
    +} + +type SidebarContentProps = { + media: MediaSidebarMedia + hidePreview?: boolean +} + +const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { + const { updateSidebar } = useContext(SidebarContext) + const { t } = useTranslation() + let previewImage = null + if (media.highRes) previewImage = media.highRes + else if (media.thumbnail) previewImage = media.thumbnail + + const imageAspect = + previewImage?.width && previewImage?.height + ? previewImage.height / previewImage.width + : 3 / 2 + + let sidebarMap = null + const mediaCoordinates = media.exif?.coordinates + if (mediaCoordinates) { + sidebarMap = + } + + let albumPath = null + const mediaAlbum = media.album + if (!isNil(mediaAlbum)) { + console.log('PATH reversed', mediaAlbum.path ?? []) + const pathElms = [ + ...[...(mediaAlbum.path ?? [])].reverse(), + mediaAlbum, + ].map(album => ( +
  • + updateSidebar(null)} + > + {album.title} + +
  • + )) + + albumPath = ( +
    +

    + {t('sidebar.media.album_path', 'Album path')} +

    + {pathElms} +
    + ) + } + + return ( +
    + +
    + {!hidePreview && ( +
    + + +
    + )} +
    + + {albumPath} + + {sidebarMap} + + +
    + +
    +
    + ) +} + +export interface MediaSidebarMedia { + __typename: 'Media' + id: string + title?: string + type: MediaType + highRes?: null | { + __typename: 'MediaURL' + url: string + width?: number + height?: number + } + thumbnail?: sidebarMediaQuery_media_thumbnail | null + videoWeb?: null | { + __typename: 'MediaURL' + url: string + width?: number + height?: number + } + videoMetadata?: sidebarMediaQuery_media_videoMetadata | null + exif?: sidebarMediaQuery_media_exif | null + faces?: sidebarMediaQuery_media_faces[] + downloads?: sidebarDownloadQuery_media_downloads[] + album?: { + __typename: 'Album' + id: string + title: string + path?: sidebarMediaQuery_media_album_path[] + } +} + +type MediaSidebarType = { + media: MediaSidebarMedia + hidePreview?: boolean +} + +const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => { + const [loadMedia, { loading, error, data }] = useLazyQuery< + sidebarMediaQuery, + sidebarMediaQueryVariables + >(SIDEBAR_MEDIA_QUERY) + + useEffect(() => { + if (media != null && authToken()) { + loadMedia({ + variables: { + id: media.id, + }, + }) + } + }, [media]) + + if (!media) return null + + if (!authToken()) { + return + } + + if (error) return
    {error.message}
    + + if (loading || data == null) { + return + } + + return +} + +export default MediaSidebar diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.tsx new file mode 100644 index 0000000..102ef39 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.tsx @@ -0,0 +1,553 @@ +import { vi } from 'vitest' +// Mock react-i18next following the project pattern +const mockUseTranslation = vi.fn() +vi.mock('react-i18next', () => ({ + useTranslation: mockUseTranslation, +})) +mockUseTranslation.mockReturnValue({ + t: (key: string, defaultValue: string) => defaultValue, + i18n: { language: 'en' }, +}) + +import React from 'react' +import { render, screen } from '@testing-library/react' +import ExifDetails from './MediaSidebarExif' +import { MediaSidebarMedia } from './MediaSidebar' +import { MediaType } from '../../../__generated__/globalTypes' + +describe('ExifDetails', () => { + test('without EXIF information', () => { + const media: MediaSidebarMedia = { + id: '1730', + title: 'media_name.jpg', + type: MediaType.Photo, + exif: { + id: '0', + description: null, + camera: null, + maker: null, + lens: null, + dateShot: null, + exposure: null, + aperture: null, + iso: null, + focalLength: null, + flash: null, + exposureProgram: null, + coordinates: null, + __typename: 'MediaEXIF', + }, + __typename: 'Media', + } + + render() + + expect(screen.queryByText('Description')).not.toBeInTheDocument() + expect(screen.queryByText('Camera')).not.toBeInTheDocument() + expect(screen.queryByText('Maker')).not.toBeInTheDocument() + expect(screen.queryByText('Lens')).not.toBeInTheDocument() + expect(screen.queryByText('Program')).not.toBeInTheDocument() + expect(screen.queryByText('Date shot')).not.toBeInTheDocument() + expect(screen.queryByText('Exposure')).not.toBeInTheDocument() + expect(screen.queryByText('Aperture')).not.toBeInTheDocument() + expect(screen.queryByText('ISO')).not.toBeInTheDocument() + expect(screen.queryByText('Focal length')).not.toBeInTheDocument() + expect(screen.queryByText('Flash')).not.toBeInTheDocument() + expect(screen.queryByText('Coordinates')).not.toBeInTheDocument() + }) + + test('with EXIF information', () => { + const media: MediaSidebarMedia = { + id: '1730', + title: 'media_name.jpg', + type: MediaType.Photo, + exif: { + id: '1666', + description: 'Media description', + camera: 'Canon EOS R', + maker: 'Canon', + lens: 'TAMRON SP 24-70mm F/2.8', + dateShot: '2021-01-23T20:50:18Z', + exposure: 0.016666666666666666, + aperture: 2.8, + iso: 100, + focalLength: 24, + flash: 9, + exposureProgram: 3, + coordinates: { + __typename: 'Coordinates', + latitude: 41.40338, + longitude: 2.17403, + }, + __typename: 'MediaEXIF', + }, + __typename: 'Media', + } + + render() + + expect(screen.getByText('Description')).toBeInTheDocument() + + expect(screen.getByText('Camera')).toBeInTheDocument() + expect(screen.getByText('Canon EOS R')).toBeInTheDocument() + + expect(screen.getByText('Maker')).toBeInTheDocument() + expect(screen.getByText('Canon')).toBeInTheDocument() + + expect(screen.getByText('Lens')).toBeInTheDocument() + expect(screen.getByText('TAMRON SP 24-70mm F/2.8')).toBeInTheDocument() + + expect(screen.getByText('Date shot')).toBeInTheDocument() + + expect(screen.getByText('Exposure')).toBeInTheDocument() + expect(screen.getByText('1/60')).toBeInTheDocument() + + expect(screen.getByText('Program')).toBeInTheDocument() + expect(screen.getByText('Aperture priority')).toBeInTheDocument() + + expect(screen.getByText('Aperture')).toBeInTheDocument() + expect(screen.getByText('f/2.8')).toBeInTheDocument() + + expect(screen.getByText('ISO')).toBeInTheDocument() + expect(screen.getByText('100')).toBeInTheDocument() + + expect(screen.getByText('Focal length')).toBeInTheDocument() + expect(screen.getByText('24mm')).toBeInTheDocument() + + expect(screen.getByText('Flash')).toBeInTheDocument() + expect(screen.getByText('On, Fired')).toBeInTheDocument() + + expect(screen.getByText('Coordinates')).toBeInTheDocument() + expect(screen.getByText('41.40338, 2.17403')).toBeInTheDocument() + }) +}) + +describe('ExifDetails dateShot formatting', () => { + + const createMediaWithDateShot = (dateShot: string): MediaSidebarMedia => ({ + id: '1730', + title: 'media_name.jpg', + type: MediaType.Photo, + exif: { + id: '1666', + description: null, + camera: null, + maker: null, + lens: null, + dateShot, + exposure: null, + aperture: null, + iso: null, + focalLength: null, + flash: null, + exposureProgram: null, + coordinates: null, + __typename: 'MediaEXIF', + }, + __typename: 'Media', + }) + + describe('RFC3339 with timezone offset', () => { + test('formats RFC3339 with positive timezone offset', () => { + const media = createMediaWithDateShot('2023-07-15T14:30:45+02:00') + render() + + expect(screen.getByText('Date shot')).toBeInTheDocument() + // Should show medium date format, time, and timezone offset WITHOUT abbreviation + expect(screen.getByText(/Jul 15, 2023.*2:30:45 PM.*\+02:00$/)).toBeInTheDocument() + }) + + test('formats RFC3339 with negative timezone offset', () => { + const media = createMediaWithDateShot('2023-12-15T10:15:30-08:00') + render() + + expect(screen.getByText(/Dec 15, 2023.*10:15:30 AM.*-08:00$/)).toBeInTheDocument() + }) + + test('formats RFC3339 with fractional timezone offset', () => { + const media = createMediaWithDateShot('2023-06-01T16:45:12+05:30') + render() + + // India Standard Time with +05:30 offset, no abbreviation + expect(screen.getByText(/Jun 1, 2023.*4:45:12 PM.*\+05:30$/)).toBeInTheDocument() + }) + + test('formats RFC3339 UTC with Z suffix', () => { + const media = createMediaWithDateShot('2023-05-10T12:00:00Z') + render() + + // Should show UTC offset but no abbreviation + expect(screen.getByText(/May 10, 2023.*12:00:00 PM.*\+00:00$/)).toBeInTheDocument() + }) + + test('formats RFC3339 with +0000 format', () => { + const media = createMediaWithDateShot('2023-01-01T12:00:00+0000') + render() + + // Should detect +0000 as timezone and show +00:00 offset + expect(screen.getByText(/Jan 1, 2023.*12:00:00 PM.*\+00:00$/)).toBeInTheDocument() + }) + }) + + describe('RFC3339 without timezone - no offset should be shown', () => { + test('formats RFC3339 without timezone shows only date and time', () => { + const media = createMediaWithDateShot('2023-08-25T18:45:30') + render() + + expect(screen.getByText('Date shot')).toBeInTheDocument() + // Should show only date and time, no timezone info + expect(screen.getByText(/^Aug 25, 2023 6:45:30 PM$/)).toBeInTheDocument() + expect(screen.queryByText(/[+-]\d{2}:\d{2}/)).not.toBeInTheDocument() + }) + + test('formats RFC3339 with milliseconds but no timezone', () => { + const media = createMediaWithDateShot('2023-03-20T09:30:15.123') + render() + + // Should show only date and time, no timezone info + expect(screen.getByText(/^Mar 20, 2023 9:30:15 AM$/)).toBeInTheDocument() + expect(screen.queryByText(/[+-]\d{2}:\d{2}/)).not.toBeInTheDocument() + }) + + test('handles trimming of whitespace in dateShot', () => { + const media = createMediaWithDateShot(' 2023-04-10T15:20:00 ') + render() + + // Should trim whitespace and format correctly without timezone + expect(screen.getByText(/^Apr 10, 2023 3:20:00 PM$/)).toBeInTheDocument() + expect(screen.queryByText(/[+-]\d{2}:\d{2}/)).not.toBeInTheDocument() + }) + }) + + describe('Translation language-based formatting', () => { + const mockUseTranslationWithLanguage = (language: string) => { + mockUseTranslation.mockReturnValue({ + t: (key: string, defaultValue: string) => defaultValue, + i18n: { language }, + }) + } + + afterEach(() => { + mockUseTranslation.mockReturnValue({ + t: (key: string, defaultValue: string) => defaultValue, + i18n: { language: 'en' }, + }) + }) + + test('uses English (en) translation language', () => { + mockUseTranslationWithLanguage('en') + + const mediaWithTz = createMediaWithDateShot('2023-11-05T15:20:10+01:00') + const { rerender } = render() + + // English locale DATE_MED format with timezone + expect(screen.getByText(/Nov 5, 2023.*3:20:10 PM.*\+01:00$/)).toBeInTheDocument() + + // Test without timezone - should use English formatting but no timezone + const mediaNoTz = createMediaWithDateShot('2023-11-05T15:20:10') + rerender() + + expect(screen.getByText(/^Nov 5, 2023 3:20:10 PM$/)).toBeInTheDocument() + expect(screen.queryByText(/[+-]\d{2}:\d{2}/)).not.toBeInTheDocument() + }) + + test('uses German (de) translation language', () => { + mockUseTranslationWithLanguage('de') + + const mediaWithTz = createMediaWithDateShot('2023-11-05T15:20:10+01:00') + const { rerender } = render() + + // German locale DATE_MED format with timezone + expect(screen.getByText(/5\. Nov\. 2023.*15:20:10.*\+01:00$/)).toBeInTheDocument() + + // Test without timezone - should use German formatting but no timezone + const mediaNoTz = createMediaWithDateShot('2023-11-05T15:20:10') + rerender() + + expect(screen.getByText(/^5\. Nov\. 2023 15:20:10$/)).toBeInTheDocument() + expect(screen.queryByText(/[+-]\d{2}:\d{2}/)).not.toBeInTheDocument() + }) + + test('uses French (fr) translation language', () => { + mockUseTranslationWithLanguage('fr') + + const mediaWithTz = createMediaWithDateShot('2023-12-25T20:45:30+01:00') + const { rerender } = render() + + // French DATE_MED format with timezone + expect(screen.getByText(/25 déc\. 2023.*20:45:30.*\+01:00$/)).toBeInTheDocument() + + // Test without timezone + const mediaNoTz = createMediaWithDateShot('2023-12-25T20:45:30') + rerender() + + expect(screen.getByText(/^25 déc\. 2023 20:45:30$/)).toBeInTheDocument() + }) + + test('uses Spanish (es) translation language', () => { + mockUseTranslationWithLanguage('es') + + const media = createMediaWithDateShot('2023-07-20T14:15:30-05:00') + render() + + // Spanish locale formatting with timezone + expect(screen.getByText(/20 jul 2023.*14:15:30.*-05:00$/)).toBeInTheDocument() + }) + + test('uses Danish (da) translation language', () => { + mockUseTranslationWithLanguage('da') + + const media = createMediaWithDateShot('2023-06-15T12:30:45+02:00') + render() + + // Danish locale formatting with timezone + expect(screen.getByText(/15\. jun\. 2023 12\.30\.45 \+02:00$/)).toBeInTheDocument() + }) + + test('uses Italian (it) translation language', () => { + mockUseTranslationWithLanguage('it') + + const media = createMediaWithDateShot('2023-09-10T11:20:15+02:00') + render() + + // Italian locale formatting with timezone + expect(screen.getByText(/10 set 2023.*11:20:15.*\+02:00$/)).toBeInTheDocument() + }) + + test('uses Portuguese (pt) translation language', () => { + mockUseTranslationWithLanguage('pt') + + const media = createMediaWithDateShot('2023-09-10T11:20:15-03:00') + render() + + // Portuguese locale formatting with timezone + expect(screen.getByText(/10 de set\. de 2023.*11:20:15.*-03:00$/)).toBeInTheDocument() + }) + + test('uses Polish (pl) translation language', () => { + mockUseTranslationWithLanguage('pl') + + const media = createMediaWithDateShot('2023-08-12T16:00:00+02:00') + render() + + // Polish locale formatting with timezone + expect(screen.getByText(/12 sie 2023.*16:00:00.*\+02:00$/)).toBeInTheDocument() + }) + + test('uses Ukrainian (uk) translation language', () => { + mockUseTranslationWithLanguage('uk') + + const media = createMediaWithDateShot('2023-08-12T16:00:00+03:00') + render() + + // Ukrainian locale formatting with timezone + expect(screen.getByText(/12 серп\. 2023.*16:00:00.*\+03:00$/)).toBeInTheDocument() + }) + + test('uses Swedish (sv) translation language', () => { + mockUseTranslationWithLanguage('sv') + + const media = createMediaWithDateShot('2023-05-15T14:30:00+02:00') + render() + + // Swedish locale formatting with timezone + expect(screen.getByText(/15 maj 2023.*14:30:00.*\+02:00$/)).toBeInTheDocument() + }) + + test('uses Japanese (ja) translation language', () => { + mockUseTranslationWithLanguage('ja') + + const media = createMediaWithDateShot('2023-04-12T08:15:45-07:00') + render() + + // Japanese locale formatting with timezone + expect(screen.getByText(/2023年4月12日.*8:15:45.*-07:00$/)).toBeInTheDocument() + }) + + test('uses Basque (eu) translation language', () => { + mockUseTranslationWithLanguage('eu') + + const media = createMediaWithDateShot('2023-03-20T10:30:00+01:00') + render() + + // Basque locale formatting with timezone + expect(screen.getByText(/2023.*mar.*20.*10:30:00.*\+01:00$/)).toBeInTheDocument() + }) + + test('handles invalid translation language gracefully', () => { + mockUseTranslationWithLanguage('invalid-locale') + + const media = createMediaWithDateShot('2023-05-15T14:30:00+01:00') + render() + + // Should fall back gracefully and still format the date + expect(screen.getByText('Date shot')).toBeInTheDocument() + expect(screen.getByText(/May 15, 2023.*2:30:00 PM.*\+01:00$/)).toBeInTheDocument() + }) + + test('handles empty string translation language', () => { + mockUseTranslationWithLanguage('') + + const media = createMediaWithDateShot('2023-01-15T12:00:00-05:00') + render() + + // Should handle empty string gracefully + expect(screen.getByText('Date shot')).toBeInTheDocument() + expect(screen.getByText(/Jan 15, 2023.*12:00:00 PM.*-05:00$/)).toBeInTheDocument() + }) + + test('uses Russian (ru) translation language', () => { + mockUseTranslationWithLanguage('ru') + + const media = createMediaWithDateShot('2023-02-23T12:15:30+03:00') + render() + + // Russian locale formatting with timezone + expect(screen.getByText(/23 февр\. 2023.*12:15:30.*\+03:00$/)).toBeInTheDocument() + }) + + test('uses Traditional Chinese Hong Kong (zh-HK) translation language', () => { + mockUseTranslationWithLanguage('zh-HK') + + const media = createMediaWithDateShot('2023-10-01T15:30:00+08:00') + render() + + // Traditional Chinese HK locale formatting with timezone + expect(screen.getByText(/2023年10月1日 下午3:30:00 \+08:00$/)).toBeInTheDocument() + }) + + test('uses Traditional Chinese Taiwan (zh-TW) translation language', () => { + mockUseTranslationWithLanguage('zh-TW') + + const media = createMediaWithDateShot('2023-10-10T16:45:00+08:00') + render() + + // Traditional Chinese TW locale formatting with timezone + expect(screen.getByText(/2023年10月10日 下午4:45:00 \+08:00$/)).toBeInTheDocument() + }) + + test('uses Simplified Chinese (zh-CN) translation language', () => { + mockUseTranslationWithLanguage('zh-CN') + + const media = createMediaWithDateShot('2023-10-01T15:30:00+08:00') + render() + + // Simplified Chinese locale formatting with timezone + expect(screen.getByText(/2023年10月1日.*15:30:00.*\+08:00$/)).toBeInTheDocument() + }) + + test('uses Turkish (tr) translation language', () => { + mockUseTranslationWithLanguage('tr') + + const media = createMediaWithDateShot('2023-05-19T13:45:25+03:00') + render() + + // Turkish locale formatting with timezone + expect(screen.getByText(/19 May 2023.*13:45:25.*\+03:00$/)).toBeInTheDocument() + }) + }) + + describe('Timezone detection edge cases', () => { + test('detects various timezone formats correctly with regex', () => { + const testCases = [ + { input: '2023-01-01T12:00:00+0000', shouldHaveTz: true }, // +HHMM format + { input: '2023-01-01T12:00:00-05:00', shouldHaveTz: true }, // -HH:MM format + { input: '2023-01-01T12:00:00Z', shouldHaveTz: true }, // Z format + { input: '2023-01-01T12:00:00z', shouldHaveTz: true }, // lowercase z + { input: '2023-01-01T12:00:00+1400', shouldHaveTz: true }, // +HHMM max offset + { input: '2023-01-01T12:00:00', shouldHaveTz: false }, // No timezone + { input: '2023-01-01T12:00:00.123', shouldHaveTz: false }, // Milliseconds, no timezone + { input: '2023-01-01T12:00:00.456789', shouldHaveTz: false }, // Microseconds, no timezone + ] + + testCases.forEach(({ input, shouldHaveTz }, index) => { + const media = createMediaWithDateShot(input) + const { rerender } = render() + + if (shouldHaveTz) { + expect(screen.getByText(/[+-]\d{2}:\d{2}$/)).toBeInTheDocument() + } else { + expect(screen.getByText(/^\w+ \d+, \d+ \d+:\d+:\d+ [AP]M$/)).toBeInTheDocument() + expect(screen.queryByText(/[+-]\d{2}:\d{2}/)).not.toBeInTheDocument() + } + + if (index < testCases.length - 1) { + rerender(
    ) + } + }) + }) + + test('handles edge case timezone offsets', () => { + const testCases = [ + { input: '2023-01-01T12:00:00+14:00', expected: /Jan 1, 2023.*12:00:00 PM.*\+14:00$/ }, // Max positive + { input: '2023-01-01T12:00:00-12:00', expected: /Jan 1, 2023.*12:00:00 PM.*-12:00$/ }, // Max negative + { input: '2023-01-01T12:00:00+05:45', expected: /Jan 1, 2023.*12:00:00 PM.*\+05:45$/ }, // Nepal time + { input: '2023-01-01T12:00:00+09:30', expected: /Jan 1, 2023.*12:00:00 PM.*\+09:30$/ }, // Adelaide + ] + + testCases.forEach(({ input, expected }, index) => { + const media = createMediaWithDateShot(input) + const { rerender } = render() + + expect(screen.getByText(expected)).toBeInTheDocument() + + if (index < testCases.length - 1) { + rerender(
    ) + } + }) + }) + }) + + describe('Error handling and robustness', () => { + test('handles invalid dateShot gracefully', () => { + const media = createMediaWithDateShot('invalid-date-string') + render() + + // Should display the original invalid string when parsing fails + expect(screen.getByText('invalid-date-string')).toBeInTheDocument() + }) + + test('handles malformed but parseable dates', () => { + // Test dates that might be malformed but still parseable + const testCases = [ + '2023-02-29T10:00:00', // Invalid leap year date + '2023-13-01T10:00:00', // Invalid month + '2023-01-32T10:00:00', // Invalid day + ] + + testCases.forEach((input, index) => { + const media = createMediaWithDateShot(input) + const { rerender } = render() + + // Should show original string if invalid + expect(screen.getByText('Date shot')).toBeInTheDocument() + expect(screen.getByText(input)).toBeInTheDocument() + + if (index < testCases.length - 1) { + rerender(
    ) + } + }) + }) + }) + + describe('Format consistency validation', () => { + test('always follows consistent format structure', () => { + const testCases = [ + { input: '2023-04-10T15:20:25-07:00', pattern: /^Apr 10, 2023 3:20:25 PM -07:00$/ }, + { input: '2023-04-10T15:20:25', pattern: /^Apr 10, 2023 3:20:25 PM$/ }, + { input: '2023-04-10T15:20:25Z', pattern: /^Apr 10, 2023 3:20:25 PM \+00:00$/ }, + ] + + testCases.forEach(({ input, pattern }, index) => { + const media = createMediaWithDateShot(input) + const { rerender } = render() + + expect(screen.getByText(pattern)).toBeInTheDocument() + + if (index < testCases.length - 1) { + rerender(
    ) + } + }) + }) + }) +}) diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.tsx new file mode 100644 index 0000000..489869c --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.tsx @@ -0,0 +1,242 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { isNil } from '../../../helpers/utils' +import { TranslationFn } from '../../../localization' +import SidebarItem from '../SidebarItem' +import { MediaSidebarMedia } from './MediaSidebar' +import { DateTime } from 'luxon' + +const MetadataInfoContainer = styled.div` + margin-bottom: 1.5rem; +` + +type ExifDetailsProps = { + media?: MediaSidebarMedia +} + +const ExifDetails = ({ media }: ExifDetailsProps) => { + const { t, i18n } = useTranslation() + let exifItems: JSX.Element[] = [] + + const exifName = exifNameLookup(t) + + if (media?.exif) { + const mediaExif = media?.exif as unknown as { + [key: string]: string | number | null + } + + const exifKeys = Object.keys(exifName).filter( + x => mediaExif[x] !== null && x != '__typename' + ) + + const exif = exifKeys.reduce((prev, curr) => { + const value = mediaExif[curr] + if (isNil(value)) return prev + + return { + ...prev, + [curr]: value, + } + }, {} as { [key: string]: string | number }) + + if (exif.dateShot && String(exif.dateShot).trim().length > 0) { + exif.dateShot = String(exif.dateShot).trim(); + const dateString = String(exif.dateShot); + + // Parse as ISO (RFC3339 is a subset of ISO8601) + const dt = DateTime.fromISO(dateString, { setZone: true }); + + if (dt.isValid) { + // Format date and time parts in user's translation locale, but as "naive" (no shifting) + const dtLocalized = dt.setLocale(i18n.language); + const localeDate = dtLocalized.toLocaleString(DateTime.DATE_MED); + const localeTime = dtLocalized.toLocaleString(DateTime.TIME_WITH_SECONDS); + + // Check if the original string contains timezone information + const hasTimezone = /(?:[zZ]|[+-]\d{2}:\d{2}|[+-]\d{4})$/.test(dateString); + if (hasTimezone) { + // Get the offset in ±HH:MM + const offset = dt.toFormat('ZZ'); + exif.dateShot = `${localeDate} ${localeTime} ${offset}`; + } else { + // No timezone in original - show only date and time + exif.dateShot = `${localeDate} ${localeTime}`; + } + } + } + + if (typeof exif.exposure === 'number' && exif.exposure !== 0) { + exif.exposure = `1/${Math.round(1 / exif.exposure)}` + } + + const coords = media.exif.coordinates + if (!isNil(coords)) { + exif.coordinates = + `${Math.round(coords.latitude * 1000000) / 1000000}, ${Math.round(coords.longitude * 1000000) / 1000000}` + } + + const exposurePrograms = exposureProgramsLookup(t) + + if ( + typeof exif.exposureProgram === 'number' && + exposurePrograms[exif.exposureProgram] + ) { + exif.exposureProgram = exposurePrograms[exif.exposureProgram] + } else if (exif.exposureProgram !== 0) { + delete exif.exposureProgram + } + + if (!isNil(exif.aperture)) { + exif.aperture = `f/${exif.aperture}` + } + + if (!isNil(exif.focalLength)) { + exif.focalLength = `${exif.focalLength}mm` + } + + const flash = flashLookup(t) + if (typeof exif.flash === 'number' && flash[exif.flash]) { + exif.flash = flash[exif.flash] + } + + exifItems = exifKeys.map(key => ( + + )) + } + + let videoMetadataItems: JSX.Element[] = [] + if (media?.videoMetadata) { + const videoMetadata = media.videoMetadata as unknown as { + [key: string]: string | number | null + } + + let metadata = Object.keys(videoMetadata) + .filter(x => !['id', '__typename', 'width', 'height'].includes(x)) + .reduce((prev, curr) => { + const value = videoMetadata[curr] + if (isNil(value)) return prev + + return { + ...prev, + [curr]: value, + } + }, {} as { [key: string]: string | number }) + + metadata = { + dimensions: `${media.videoMetadata.width}x${media.videoMetadata.height}`, + ...metadata, + } + + videoMetadataItems = Object.keys(metadata).map(key => ( + + )) + } + + return ( +
    + {videoMetadataItems} + {exifItems} +
    + ) +} + +const exifNameLookup = (t: TranslationFn): { [key: string]: string } => ({ + description: t('sidebar.media.exif.description', 'Description'), + camera: t('sidebar.media.exif.name.camera', 'Camera'), + maker: t('sidebar.media.exif.name.maker', 'Maker'), + lens: t('sidebar.media.exif.name.lens', 'Lens'), + exposureProgram: t('sidebar.media.exif.name.exposure_program', 'Program'), + dateShot: t('sidebar.media.exif.name.date_shot', 'Date shot'), + exposure: t('sidebar.media.exif.name.exposure', 'Exposure'), + aperture: t('sidebar.media.exif.name.aperture', 'Aperture'), + iso: t('sidebar.media.exif.name.iso', 'ISO'), + focalLength: t('sidebar.media.exif.name.focal_length', 'Focal length'), + flash: t('sidebar.media.exif.name.flash', 'Flash'), + coordinates: t('sidebar.media.exif.name.coordinates', 'Coordinates'), +}) + +// From https://exiftool.org/TagNames/EXIF.html +const exposureProgramsLookup = ( + t: TranslationFn +): { [key: number]: string } => ({ + 0: t('sidebar.media.exif.exposure_program.not_defined', 'Not defined'), + 1: t('sidebar.media.exif.exposure_program.manual', 'Manual'), + 2: t('sidebar.media.exif.exposure_program.normal_program', 'Normal program'), + 3: t( + 'sidebar.media.exif.exposure_program.aperture_priority', + 'Aperture priority' + ), + 4: t( + 'sidebar.media.exif.exposure_program.shutter_priority', + 'Shutter priority' + ), + 5: t( + 'sidebar.media.exif.exposure_program.creative_program', + 'Creative program' + ), + 6: t('sidebar.media.exif.exposure_program.action_program', 'Action program'), + 7: t('sidebar.media.exif.exposure_program.portrait_mode', 'Portrait mode'), + 8: t('sidebar.media.exif.exposure_program.landscape_mode', 'Landscape mode'), + 9: t('sidebar.media.exif.exposure_program.bulb', 'Bulb'), +}) + +// From https://exiftool.org/TagNames/EXIF.html#Flash +const flashLookup = (t: TranslationFn): { [key: number]: string } => { + const values = { + no_flash: t('sidebar.media.exif.flash.no_flash', 'No Flash'), + fired: t('sidebar.media.exif.flash.fired', 'Fired'), + did_not_fire: t('sidebar.media.exif.flash.did_not_fire', 'Did not fire'), + on: t('sidebar.media.exif.flash.on', 'On'), + off: t('sidebar.media.exif.flash.off', 'Off'), + auto: t('sidebar.media.exif.flash.auto', 'Auto'), + return_not_detected: t( + 'sidebar.media.exif.flash.return_not_detected', + 'Return not detected' + ), + return_detected: t( + 'sidebar.media.exif.flash.return_detected', + 'Return detected' + ), + no_flash_function: t( + 'sidebar.media.exif.flash.no_flash_function', + 'No flash function' + ), + red_eye_reduction: t( + 'sidebar.media.exif.flash.red_eye_reduction', + 'Red-eye reduction' + ), + } + + return { + 0x0: values['no_flash'], + 0x1: values['fired'], + 0x5: `${values['fired']}, ${values['return_not_detected']}`, + 0x7: `${values['fired']}, ${values['return_detected']}`, + 0x8: `${values['on']}, ${values['did_not_fire']}`, + 0x9: `${values['on']}, ${values['fired']}`, + 0xd: `${values['on']}, ${values['return_not_detected']}`, + 0xf: `${values['on']}, ${values['return_detected']}`, + 0x10: `${values['off']}, ${values['did_not_fire']}`, + 0x14: `${values['off']}, ${values['did_not_fire']}, ${values['return_not_detected']}`, + 0x18: `${values['auto']}, ${values['did_not_fire']}`, + 0x19: `${values['auto']}, ${values['fired']}`, + 0x1d: `${values['auto']}, ${values['fired']}, ${values['return_not_detected']}`, + 0x1f: `${values['auto']}, ${values['fired']}, ${values['return_detected']}`, + 0x20: `${values['no_flash_function']}`, + 0x30: `${values['off']}, ${values['no_flash_function']}`, + 0x41: `${values['fired']}, ${values['red_eye_reduction']}`, + 0x45: `${values['fired']}, ${values['red_eye_reduction']}, ${values['return_not_detected']}`, + 0x47: `${values['fired']}, ${values['red_eye_reduction']}, ${values['return_detected']}`, + 0x49: `${values['on']}, ${values['red_eye_reduction']}`, + 0x4d: `${values['on']}, ${values['red_eye_reduction']}, ${values['return_not_detected']}`, + 0x4f: `${values['on']}, ${values['red_eye_reduction']}, ${values['return_detected']}`, + 0x50: `${values['off']}, ${values['red_eye_reduction']}`, + 0x58: `${values['auto']}, ${values['did_not_fire']}, ${values['red_eye_reduction']}`, + 0x59: `${values['auto']}, ${values['fired']}, ${values['red_eye_reduction']}`, + 0x5d: `${values['auto']}, ${values['red_eye_reduction']}, ${values['return_not_detected']}`, + 0x5f: `${values['auto']}, ${values['red_eye_reduction']}, ${values['return_detected']}`, + } +} + +export default ExifDetails diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx new file mode 100644 index 0000000..1fa0f2c --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { isNil } from '../../../helpers/utils' +import useMapboxMap from '../../mapbox/MapboxMap' +import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' +import { sidebarMediaQuery_media_exif_coordinates } from './__generated__/sidebarMediaQuery' + +type MediaSidebarMapProps = { + coordinates: sidebarMediaQuery_media_exif_coordinates +} + +const MediaSidebarMap = ({ coordinates }: MediaSidebarMapProps) => { + const { t } = useTranslation() + + const { mapContainer, mapboxToken } = useMapboxMap({ + mapboxOptions: { + interactive: false, + zoom: 12, + center: { + lat: coordinates.latitude, + lng: coordinates.longitude, + }, + }, + configureMapbox: (map, mapboxLibrary) => { + // todo + map.addControl( + new mapboxLibrary.NavigationControl({ showCompass: false }) + ) + + const centerMarker = new mapboxLibrary.Marker({ + color: 'red', + scale: 0.8, + }) + centerMarker.setLngLat({ + lat: coordinates.latitude, + lng: coordinates.longitude, + }) + centerMarker.addTo(map) + }, + }) + + if (isNil(mapboxToken)) { + return null + } + + return ( + + + {t('sidebar.location.title', 'Location')} + +
    {mapContainer}
    +
    + ) +} + +export default MediaSidebarMap diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx new file mode 100644 index 0000000..803b849 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Link, useNavigate } from 'react-router-dom' +import FaceCircleImage from '../../../Pages/PeoplePage/FaceCircleImage' +import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' +import { MediaSidebarMedia, SIDEBAR_MEDIA_QUERY } from './MediaSidebar' +import { sidebarMediaQuery_media_faces } from './__generated__/sidebarMediaQuery' + +import { ReactComponent as PeopleDotsIcon } from './icons/peopleDotsIcon.svg' +import { Menu } from '@headlessui/react' +import { Button } from '../../../primitives/form/Input' +import { ArrowPopoverPanel } from '../Sharing' +import { isNil, tailwindClassNames } from '../../../helpers/utils' +import MergeFaceGroupsModal, { + MergeFaceGroupsModalState, +} from '../../../Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal' +import { useDetachImageFaces } from '../../../Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal' +import MoveImageFacesModal from '../../../Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal' +import { FaceDetails } from '../../../Pages/PeoplePage/PeoplePage' + +type PersonMoreMenuItemProps = { + label: string + className?: string + onClick(): void +} + +const PersonMoreMenuItem = ({ + label, + className, + onClick, +}: PersonMoreMenuItemProps) => { + return ( + + {({ active }) => ( + + )} + + ) +} + +type PersonMoreMenuProps = { + face: sidebarMediaQuery_media_faces + setChangeLabel: React.Dispatch> + className?: string + menuFlipped: boolean +} + +const PersonMoreMenu = ({ + menuFlipped, + face, + setChangeLabel, + className, +}: PersonMoreMenuProps) => { + const { t } = useTranslation() + + const [mergeModalState, setMergeModalState] = useState( + MergeFaceGroupsModalState.Closed + ) + const [moveModalOpen, setMoveModalOpen] = useState(false) + + const refetchQueries = [ + { + query: SIDEBAR_MEDIA_QUERY, + variables: { + id: face.media.id, + }, + }, + ] + + const navigate = useNavigate() + const detachImageFaceMutation = useDetachImageFaces({ + refetchQueries, + }) + + const modals = ( + <> + + + + ) + + const detachImageFace = () => { + if ( + !confirm( + t( + 'sidebar.people.confirm_image_detach', + 'Are you sure you want to detach this image?' + ) + ) + ) + return + detachImageFaceMutation([face]).then(({ data }) => { + if (isNil(data)) throw new Error('Expected data not to be null') + navigate(`/people/${data.detachImageFaces.id}`) + }) + } + + return ( + <> + + + + + + + setChangeLabel(true)} + className="border-b" + label={t('people_page.action_label.change_label', 'Change label')} + /> + + setMergeModalState(MergeFaceGroupsModalState.SelectDestination) + } + className="border-b" + label={t('sidebar.people.action_label.merge_face', 'Merge face')} + /> + detachImageFace()} + className="border-b" + label={t( + 'sidebar.people.action_label.detach_image', + 'Detach image' + )} + /> + setMoveModalOpen(true)} + label={t('sidebar.people.action_label.move_face', 'Move face')} + /> + + + + {modals} + + ) +} + +type MediaSidebarFaceProps = { + face: sidebarMediaQuery_media_faces + menuFlipped: boolean +} + +const MediaSidebarPerson = ({ face, menuFlipped }: MediaSidebarFaceProps) => { + const [changeLabel, setChangeLabel] = useState(false) + + return ( +
  • + + + +
    + + {!changeLabel && ( + + )} +
    +
  • + ) +} + +type MediaSidebarFacesProps = { + media: MediaSidebarMedia +} + +const MediaSidebarPeople = ({ media }: MediaSidebarFacesProps) => { + const { t } = useTranslation() + + const faceElms = (media.faces ?? []).map((face, i) => ( + + )) + + if (faceElms.length == 0) return null + + return ( + + + {t('sidebar.people.title', 'People')} + +
      {faceElms}
    +
    + ) +} + +export default MediaSidebarPeople diff --git a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts new file mode 100644 index 0000000..8b93525 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts @@ -0,0 +1,257 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: sidebarMediaQuery +// ==================================================== + +export interface sidebarMediaQuery_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface sidebarMediaQuery_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface sidebarMediaQuery_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface sidebarMediaQuery_media_videoMetadata { + __typename: "VideoMetadata"; + id: string; + width: number; + height: number; + duration: number; + codec: string | null; + framerate: number | null; + bitrate: string | null; + colorProfile: string | null; + audio: string | null; +} + +export interface sidebarMediaQuery_media_exif_coordinates { + __typename: "Coordinates"; + /** + * GPS latitude in degrees + */ + latitude: number; + /** + * GPS longitude in degrees + */ + longitude: number; +} + +export interface sidebarMediaQuery_media_exif { + __typename: "MediaEXIF"; + id: string; + /** + * The description of the image + */ + description: string | null; + /** + * The model name of the camera + */ + camera: string | null; + /** + * The maker of the camera + */ + maker: string | null; + /** + * The name of the lens + */ + lens: string | null; + /** + * The date when the photo is shot + */ + dateShot: string | null; + /** + * The exposure time of the image + */ + exposure: number | null; + /** + * The aperature stops of the image + */ + aperture: number | null; + /** + * The ISO setting of the image + */ + iso: number | null; + /** + * The focal length of the lens, when the image was taken + */ + focalLength: number | null; + /** + * A formatted description of the flash settings, when the image was taken + */ + flash: number | null; + /** + * An index describing the mode for adjusting the exposure of the image + */ + exposureProgram: number | null; + /** + * GPS coordinates of where the image was taken + */ + coordinates: sidebarMediaQuery_media_exif_coordinates | null; +} + +export interface sidebarMediaQuery_media_album_path { + __typename: "Album"; + id: string; + title: string; +} + +export interface sidebarMediaQuery_media_album { + __typename: "Album"; + id: string; + title: string; + /** + * A breadcrumb list of all parent albums down to this one + */ + path: sidebarMediaQuery_media_album_path[]; +} + +export interface sidebarMediaQuery_media_faces_rectangle { + __typename: "FaceRectangle"; + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export interface sidebarMediaQuery_media_faces_faceGroup { + __typename: "FaceGroup"; + id: string; + /** + * The name of the person + */ + label: string | null; + /** + * The total number of images in this collection + */ + imageFaceCount: number; +} + +export interface sidebarMediaQuery_media_faces_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface sidebarMediaQuery_media_faces_media { + __typename: "Media"; + id: string; + title: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: sidebarMediaQuery_media_faces_media_thumbnail | null; +} + +export interface sidebarMediaQuery_media_faces { + __typename: "ImageFace"; + id: string; + /** + * A bounding box of where on the image the face is present + */ + rectangle: sidebarMediaQuery_media_faces_rectangle; + /** + * The `FaceGroup` that contains this `ImageFace` + */ + faceGroup: sidebarMediaQuery_media_faces_faceGroup; + /** + * A reference to the image the face appears on + */ + media: sidebarMediaQuery_media_faces_media; +} + +export interface sidebarMediaQuery_media { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: sidebarMediaQuery_media_highRes | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: sidebarMediaQuery_media_thumbnail | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: sidebarMediaQuery_media_videoWeb | null; + videoMetadata: sidebarMediaQuery_media_videoMetadata | null; + exif: sidebarMediaQuery_media_exif | null; + /** + * The album that holds the media + */ + album: sidebarMediaQuery_media_album; + /** + * A list of faces present on the image + */ + faces: sidebarMediaQuery_media_faces[]; +} + +export interface sidebarMediaQuery { + /** + * Get media by id, user must own the media or be admin. + * If valid tokenCredentials are provided, the media may be retrived without further authentication + */ + media: sidebarMediaQuery_media; +} + +export interface sidebarMediaQueryVariables { + id: string; +} diff --git a/ui/src/components/sidebar/MediaSidebar/icons/peopleDotsIcon.svg b/ui/src/components/sidebar/MediaSidebar/icons/peopleDotsIcon.svg new file mode 100644 index 0000000..9e8e5ba --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/icons/peopleDotsIcon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/Sharing.test.tsx b/ui/src/components/sidebar/Sharing.test.tsx new file mode 100644 index 0000000..2aa7b81 --- /dev/null +++ b/ui/src/components/sidebar/Sharing.test.tsx @@ -0,0 +1,146 @@ +import { vi, describe, test, expect } from 'vitest' +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { MockedProvider } from '@apollo/client/testing' +import { SidebarAlbumShare, SET_EXPIRE_MUTATION, SHARE_ALBUM_QUERY } from './Sharing' + +// ========================================================= +// Key modification: Mock this layer directly to bypass js-cookie. +// This prevents the test runner from reading the authentication.ts file, +// avoiding the "Failed to load js-cookie" error. +// ========================================================= +vi.mock('../../helpers/authentication', () => ({ + authToken: () => 'mock_token', + getSharePassword: () => 'mock_pass', + saveSharePassword: vi.fn(), + clearSharePassword: vi.fn(), +})) + +// Mock the translation module to prevent interference +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, fallback: string) => fallback, + i18n: { language: 'en' }, + }), +})) + +const MOCK_ALBUM_ID = 'album-123' +const MOCK_TOKEN = 'token-abc' + +const mockShareNoExpire = { + id: 'share-1', + token: MOCK_TOKEN, + hasPassword: false, + expire: null, + __typename: 'ShareToken' +} + +const mockShareWithExpire = { + ...mockShareNoExpire, + expire: '2025-01-01T00:00:00Z' +} + +const getSharesQueryMock = { + request: { + query: SHARE_ALBUM_QUERY, + variables: { id: MOCK_ALBUM_ID }, + }, + result: { + data: { + album: { + id: MOCK_ALBUM_ID, + __typename: 'Album', + shares: [mockShareNoExpire], + }, + }, + }, +} + +const setExpireMutationMock = { + request: { + query: SET_EXPIRE_MUTATION, + variables: { + token: MOCK_TOKEN, + expire: expect.stringContaining('2026-10-01'), + }, + }, + result: { + data: { + setExpireShareToken: { + token: MOCK_TOKEN, + __typename: 'ShareToken', + }, + }, + }, +} + +const clearExpireMutationMock = { + request: { + query: SET_EXPIRE_MUTATION, + variables: { + token: MOCK_TOKEN, + expire: null, + }, + }, + result: { + data: { + setExpireShareToken: { + token: MOCK_TOKEN, + __typename: 'ShareToken', + }, + }, + }, +} + +describe('Sidebar Sharing Expiration', () => { + test('User can open popover, enable expiration, set date, and save', async () => { + render( + + + + ) + + await waitFor(() => expect(screen.getByText(/Public Link/i)).toBeInTheDocument()) + + const moreBtn = screen.getByTitle('More') + fireEvent.click(moreBtn) + + const checkbox = await screen.findByLabelText(/Expiration date/i) + fireEvent.click(checkbox) + + const inputs = screen.getAllByRole('textbox') + + // Filter for the enabled input field (the password field is disabled by default) + const dateInput = inputs.find(input => !input.hasAttribute('disabled')) + + if (!dateInput) { + throw new Error('Could not find the enabled date input field') + } + + // Interact with the input + fireEvent.change(dateInput, { target: { value: '2026-10-01' } }) + fireEvent.keyDown(dateInput, { key: 'Enter', code: 'Enter', charCode: 13 }) + }) + + test('User can clear the expiration date by unchecking the box', async () => { + const mockWithExpireData = JSON.parse(JSON.stringify(getSharesQueryMock)) + mockWithExpireData.result.data.album.shares = [mockShareWithExpire] + + render( + + + + ) + + await waitFor(() => expect(screen.getByText(/Public Link/i)).toBeInTheDocument()) + + fireEvent.click(screen.getByTitle('More')) + + const checkbox = await screen.findByLabelText(/Expiration date/i) + expect(checkbox).toBeChecked() + + fireEvent.click(checkbox) + + await waitFor(() => expect(checkbox).not.toBeChecked()) + }) +}) \ No newline at end of file diff --git a/ui/src/components/sidebar/Sharing.tsx b/ui/src/components/sidebar/Sharing.tsx new file mode 100644 index 0000000..3e92cb2 --- /dev/null +++ b/ui/src/components/sidebar/Sharing.tsx @@ -0,0 +1,604 @@ +import React, { useEffect, useState } from 'react' +import { + useMutation, + useQuery, + gql, + useLazyQuery, + DocumentNode, +} from '@apollo/client' +import copy from 'copy-to-clipboard' +import { useTranslation } from 'react-i18next' +import { Popover } from '@headlessui/react' +import { + sidebareDeleteShare, + sidebareDeleteShareVariables, +} from './__generated__/sidebareDeleteShare' +import { + sidebarPhotoAddShare, + sidebarPhotoAddShareVariables, +} from './__generated__/sidebarPhotoAddShare' +import { + sidebarAlbumAddShare, + sidebarAlbumAddShareVariables, +} from './__generated__/sidebarAlbumAddShare' +import { + sidebarGetPhotoShares, + sidebarGetPhotoSharesVariables, + sidebarGetPhotoShares_media_shares, +} from './__generated__/sidebarGetPhotoShares' +import { + sidebarGetAlbumShares, + sidebarGetAlbumSharesVariables, + sidebarGetAlbumShares_album_shares, +} from './__generated__/sidebarGetAlbumShares' +import { authToken } from '../../helpers/authentication' +import { SidebarSection, SidebarSectionTitle } from './SidebarComponents' + +import { ReactComponent as LinkIcon } from './icons/shareLinkIcon.svg' +import { ReactComponent as CopyIcon } from './icons/shareCopyIcon.svg' +import { ReactComponent as DeleteIcon } from './icons/shareDeleteIcon.svg' +import { ReactComponent as MoreIcon } from './icons/shareMoreIcon.svg' +import { ReactComponent as AddIcon } from './icons/shareAddIcon.svg' +import Checkbox from '../../primitives/form/Checkbox' +import { TextField } from '../../primitives/form/Input' +import styled from 'styled-components' +import { + sidebarProtectShare, + sidebarProtectShareVariables, +} from './__generated__/sidebarProtectShare' +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import dayjs from 'dayjs' + +const SHARE_PHOTO_QUERY = gql` + query sidebarGetPhotoShares($id: ID!) { + media(id: $id) { + id + shares { + id + token + hasPassword + expire + } + } + } +` + +export const SHARE_ALBUM_QUERY = gql` + query sidebarGetAlbumShares($id: ID!) { + album(id: $id) { + id + shares { + id + token + hasPassword + expire + } + } + } +` + +const ADD_MEDIA_SHARE_MUTATION = gql` + mutation sidebarPhotoAddShare($id: ID!, $password: String, $expire: Time) { + shareMedia(mediaId: $id, password: $password, expire: $expire) { + token + } + } +` + +const ADD_ALBUM_SHARE_MUTATION = gql` + mutation sidebarAlbumAddShare($id: ID!, $password: String, $expire: Time) { + shareAlbum(albumId: $id, password: $password, expire: $expire) { + token + } + } +` + +const PROTECT_SHARE_MUTATION = gql` + mutation sidebarProtectShare($token: String!, $password: String) { + protectShareToken(token: $token, password: $password) { + token + hasPassword + } + } +` + +export const SET_EXPIRE_MUTATION = gql` + mutation sidebarSetExpireShare($token: String!, $expire: Time){ + setExpireShareToken(token: $token, expire: $expire){ + token + } + } +` + +const DELETE_SHARE_MUTATION = gql` + mutation sidebareDeleteShare($token: String!) { + deleteShareToken(token: $token) { + token + } + } +` + +export const ArrowPopoverPanel = styled.div.attrs({ + className: + 'absolute -top-3 bg-white dark:bg-dark-bg rounded shadow-md border border-gray-200 dark:border-dark-border z-10', +}) <{ width: number; flipped?: boolean }>` + width: ${({ width }) => width}px; + + ${({ flipped }) => + flipped + ? ` + left: 32px; + ` + : ` + right: 24px; + `} + + &::after { + content: ''; + position: absolute; + top: 18px; + width: 8px; + height: 14px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 14'%3E%3Cpolyline stroke-width='1' stroke='%23E2E2E2' fill='%23FFFFFF' points='1 0 7 7 1 14'%3E%3C/polyline%3E%3C/svg%3E"); + + ${({ flipped }) => + flipped + ? ` + left: -7px; + transform: rotate(180deg); + ` + : ` + right: -7px; + `} + } +` + +type MorePopoverSectionPasswordProps = { + share: sidebarGetAlbumShares_album_shares + query: DocumentNode + id: string +} + +const MorePopoverSectionPassword = ({ + share, + query, + id, +}: MorePopoverSectionPasswordProps) => { + const [addingPassword, setAddingPassword] = useState(false) + const activated = addingPassword || share.hasPassword + + const [passwordInputValue, setPasswordInputValue] = useState( + share.hasPassword ? '**********' : '' + ) + const [passwordHidden, setPasswordHidden] = useState(share.hasPassword) + + const [setPassword, { loading: setPasswordLoading }] = useMutation< + sidebarProtectShare, + sidebarProtectShareVariables + >(PROTECT_SHARE_MUTATION, { + refetchQueries: [{ query: query, variables: { id } }], + onCompleted: data => { + hidePassword(data.protectShareToken.hasPassword) + }, + // refetchQueries: [{ query: query, variables: { id } }], + variables: { + token: share.token, + }, + }) + + const hidePassword = (hide: boolean) => { + if (hide) { + setPasswordInputValue('**********') + } + + if (passwordHidden && !hide) { + setPasswordInputValue('') + } + + setPasswordHidden(hide) + } + + const checkboxChange = () => { + const enable = !activated + setAddingPassword(enable) + if (!enable) { + setPassword({ + variables: { + token: share.token, + password: null, + }, + }) + setPasswordInputValue('') + } + } + + const updatePasswordAction = () => { + if (!passwordHidden && passwordInputValue != '') { + setPassword({ + variables: { + token: share.token, + password: passwordInputValue, + }, + }) + } + } + + return ( +
    + + { + if ( + event.shiftKey || + event.altKey || + event.ctrlKey || + event.metaKey || + event.key == 'Enter' || + event.key == 'Tab' || + event.key == 'Escape' + ) { + return + } + + hidePassword(false) + }} + onChange={event => { + setPasswordInputValue(event.target.value) + }} + action={updatePasswordAction} + loading={setPasswordLoading} + /> +
    + ) +} + +type MorePopoverSectionExpirationProps = { + share: sidebarGetAlbumShares_album_shares + id: string + query: DocumentNode +} + +const MorePopoverSectionExpiration = ({ + share, + id, + query, +}: MorePopoverSectionExpirationProps) => { + // Verify whether the backend response includes an expiration time + // Set it to true if share.expire exists; otherwise,set it to false + const [enabled, setEnabled] = useState(!!share.expire) + useEffect(() => { + setEnabled(!!share.expire) + setDate(share.expire ? new Date(share.expire) : null) + }, [share.expire]) + const { t, i18n } = useTranslation() + + const dateFormatterOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + } + const dateFormatter = new Intl.DateTimeFormat( + i18n.language, + dateFormatterOptions + ) + const oldExpireDate = share.expire + ? dateFormatter.format(new Date(share.expire.slice(0, 19).replace(' ', 'T'))) + : ''; + + const [date, setDate] = useState( + share.expire ? new Date(share.expire) : null + ) + + const [setExpire, { loading }] = useMutation(SET_EXPIRE_MUTATION, { + refetchQueries: [{ query, variables: { id } }], + }) + + const submit = () => { + if (!date && enabled) return + const formatDate = date ? dayjs(date).endOf('day').format('YYYY-MM-DDTHH:mm:ss')+'Z' : null + //Save the local time while treating it as UTC. + setExpire({ + variables: { + token: share.token, + expire: formatDate, + }, + }) + } + + return ( +
    + { + const next = !enabled + setEnabled(next) + + if (!next) { + // If the checkbox is unchecked,set the expiration time to null. + setDate(null) + setExpire({ + variables: { + token: share.token, + expire: null, + }, + }) + } + }} + /> + + {enabled && ( +
    + + } + + /> +
    + )} +
    + ) +} + +type MorePopoverProps = { + id: string + query: DocumentNode + share: sidebarGetAlbumShares_album_shares +} + +const MorePopover = ({ id, share, query }: MorePopoverProps) => { + const { t } = useTranslation() + + return ( + + + + + + + + + + + + + ) +} + +type SidebarShareAlbumProps = { + id: string +} + +export const SidebarAlbumShare = ({ id }: SidebarShareAlbumProps) => { + const { t } = useTranslation() + + const { + loading: queryLoading, + error: sharesError, + data: sharesData, + } = useQuery( + SHARE_ALBUM_QUERY, + { variables: { id } } + ) + + const [shareAlbum, { loading: mutationLoading }] = useMutation< + sidebarAlbumAddShare, + sidebarAlbumAddShareVariables + >(ADD_ALBUM_SHARE_MUTATION, { + refetchQueries: [{ query: SHARE_ALBUM_QUERY, variables: { id } }], + }) + + const loading = queryLoading || mutationLoading + + if (sharesError) { + return
    Error: {sharesError.message}
    + } + + if (loading) { + return
    {t('general.loading.shares', 'Loading shares...')}
    + } + + return ( + + ) +} +type SidebarSharePhotoProps = { + id: string +} + +export const SidebarPhotoShare = ({ id }: SidebarSharePhotoProps) => { + const { t } = useTranslation() + + const [ + loadShares, + { loading: queryLoading, error: sharesError, data: sharesData }, + ] = useLazyQuery( + SHARE_PHOTO_QUERY + ) + + const [sharePhoto, { loading: mutationLoading }] = useMutation< + sidebarPhotoAddShare, + sidebarPhotoAddShareVariables + >(ADD_MEDIA_SHARE_MUTATION, { + refetchQueries: [{ query: SHARE_PHOTO_QUERY, variables: { id } }], + }) + + useEffect(() => { + if (authToken()) { + loadShares({ + variables: { + id, + }, + }) + } + }, []) + + const loading = queryLoading || mutationLoading + + if (sharesError) { + return
    Error: {sharesError.message}
    + } + + if (loading) { + return
    {t('general.loading.shares', 'Loading shares...')}
    + } + + return ( + + ) +} + +type SidebarShareProps = { + id: string + isPhoto: boolean + loading: boolean + shares?: sidebarGetPhotoShares_media_shares[] + shareItem: (item: { variables: { id: string } }) => Promise +} + +const SidebarShare = ({ + loading, + shares, + isPhoto, + id, + shareItem, +}: SidebarShareProps) => { + const { t } = useTranslation() + + const query = isPhoto ? SHARE_PHOTO_QUERY : SHARE_ALBUM_QUERY + + const [deleteShare] = useMutation< + sidebareDeleteShare, + sidebareDeleteShareVariables + >(DELETE_SHARE_MUTATION, { + refetchQueries: [{ query: query, variables: { id } }], + }) + + if (shares === undefined) { + return null + } + + const optionsRows = shares.map(share => ( + + + + + + {t('sidebar.sharing.public_link', 'Public Link') + ' '} + + + {share.token} + + + + + + + {/* */} + + + )) + + if (optionsRows.length == 0) { + optionsRows.push( + + + {t('sidebar.sharing.no_shares_found', 'No shares found')} + + + ) + } + + return ( + + + {t('sidebar.sharing.title', 'Sharing options')} + +
    + + {optionsRows} + + + + + +
    + +
    +
    +
    + ) +} \ No newline at end of file diff --git a/ui/src/components/sidebar/Sidebar.tsx b/ui/src/components/sidebar/Sidebar.tsx new file mode 100644 index 0000000..3af4a36 --- /dev/null +++ b/ui/src/components/sidebar/Sidebar.tsx @@ -0,0 +1,111 @@ +import React, { createContext, useContext, useEffect, useState } from 'react' + +export type UpdateSidebarFn = (content: React.ReactNode) => void +export type SidebarPinnedFn = (pin: boolean) => void + +interface SidebarContextType { + updateSidebar: UpdateSidebarFn + setPinned: SidebarPinnedFn + content: React.ReactNode + pinned: boolean +} + +export const SidebarContext = createContext({ + updateSidebar: content => { + console.warn( + 'SidebarContext: updateSidebar was called before initialized', + content + ) + }, + setPinned: content => { + console.warn( + 'SidebarContext: setPinned was called before initialized', + content + ) + }, + content: null, + pinned: false, +}) +SidebarContext.displayName = 'SidebarContext' + +type SidebarProviderProps = { + children: React.ReactChild | React.ReactChild[] +} + +export const SidebarProvider = ({ children }: SidebarProviderProps) => { + const [state, setState] = useState<{ + content: React.ReactNode | null + pinned: boolean + }>({ + content: null, + pinned: false, + }) + + const updateSidebar = (content: React.ReactNode | null) => { + if (content) { + setState(state => ({ ...state, content })) + } else { + setState(state => ({ ...state, content: null, pinned: false })) + } + } + + const setPinned = (pinned: boolean) => { + setState(state => ({ ...state, pinned })) + } + + return ( + + {children} + + ) +} + +export const Sidebar = () => { + const { content, pinned, updateSidebar } = useContext(SidebarContext) + + useEffect(() => { + const body = document.body + + if (content == null) { + body.classList.remove('overflow-y-hidden') + body.classList.remove('lg:overflow-y-auto') + } else { + body.classList.add('overflow-y-hidden') + body.classList.add('lg:overflow-y-auto') + } + + return () => { + body.classList.remove('overflow-y-hidden') + body.classList.remove('lg:overflow-y-auto') + } + }) + + return ( + <> + {/* Backdrop — tap to close (mobile only) */} + {content && !pinned && ( +
    updateSidebar(null)} + /> + )} +
    + {content} +
    +
    + + ) +} diff --git a/ui/src/components/sidebar/SidebarComponents.tsx b/ui/src/components/sidebar/SidebarComponents.tsx new file mode 100644 index 0000000..8a7f2d1 --- /dev/null +++ b/ui/src/components/sidebar/SidebarComponents.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components' + +export const SidebarSection = styled.div.attrs({ className: 'mb-8' })`` + +export const SidebarSectionTitle = styled.h2.attrs({ + className: 'text-xl mb-2 mx-4', +})`` diff --git a/ui/src/components/sidebar/SidebarDownloadAlbum.tsx b/ui/src/components/sidebar/SidebarDownloadAlbum.tsx new file mode 100644 index 0000000..a578e59 --- /dev/null +++ b/ui/src/components/sidebar/SidebarDownloadAlbum.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { API_ENDPOINT } from '../../apolloClient' +import { SidebarSection, SidebarSectionTitle } from './SidebarComponents' +import SidebarTable from './SidebarTable' + +type SidebarAlbumDownladProps = { + albumID: string +} + +const SidebarAlbumDownload = ({ albumID }: SidebarAlbumDownladProps) => { + const { t } = useTranslation() + + const downloads = [ + { + title: t('sidebar.album.download.thumbnails.title', 'Thumbnails'), + description: t( + 'sidebar.album.download.thumbnails.description', + 'Low resolution images, no videos' + ), + purpose: 'thumbnail,video-thumbnail', + }, + { + title: t( + 'sidebar.album.download.high-resolutions.title', + 'High resolutions' + ), + description: t( + 'sidebar.album.download.high-resolutions.description', + 'High resolution jpegs of RAW images' + ), + purpose: 'high-res', + }, + { + title: t('sidebar.album.download.originals.title', 'Originals'), + description: t( + 'sidebar.album.download.originals.description', + 'The original images and videos' + ), + purpose: 'original', + }, + { + title: t('sidebar.album.download.web-videos.title', 'Converted videos'), + description: t( + 'sidebar.album.download.web-videos.description', + 'Videos that have been optimized for web' + ), + purpose: 'video-web', + }, + ] + + const downloadRows = downloads.map(x => ( + + (location.href = `${API_ENDPOINT}/download/album/${albumID}/${x.purpose}`) + } + tabIndex={0} + > + {`${x.title}`} + {`${x.description}`} + + )) + + return ( + + + {t('sidebar.download.title', 'Download')} + + + + + + + {t('sidebar.download.table_columns.name', 'Name')} + + + + {downloadRows} + + + ) +} + +export default SidebarAlbumDownload diff --git a/ui/src/components/sidebar/SidebarDownloadMedia.tsx b/ui/src/components/sidebar/SidebarDownloadMedia.tsx new file mode 100644 index 0000000..d489df2 --- /dev/null +++ b/ui/src/components/sidebar/SidebarDownloadMedia.tsx @@ -0,0 +1,295 @@ +import { gql, useLazyQuery } from '@apollo/client' +import { useTranslation } from 'react-i18next' +import { NotificationType } from '../../__generated__/globalTypes' +import { authToken } from '../../helpers/authentication' +import { TranslationFn } from '../../localization' +import { MessageState } from '../messages/Messages' +import { MediaSidebarMedia } from './MediaSidebar/MediaSidebar' +import React from 'react' +import { SidebarSection, SidebarSectionTitle } from './SidebarComponents' +import SidebarTable from './SidebarTable' +import { + sidebarDownloadQuery, + sidebarDownloadQueryVariables, + sidebarDownloadQuery_media_downloads, +} from './__generated__/sidebarDownloadQuery' + +export const SIDEBAR_DOWNLOAD_QUERY = gql` + query sidebarDownloadQuery($mediaId: ID!) { + media(id: $mediaId) { + id + downloads { + title + mediaUrl { + url + width + height + fileSize + } + } + } + } +` + +const formatBytes = (t: TranslationFn) => (bytes: number) => { + if (bytes == 0) + return t('sidebar.download.filesize.byte', '{{count}} Byte', { count: 0 }) + + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + const count = Math.round(bytes / Math.pow(1024, i)) + + switch (i) { + case 0: + // i18next-extract-mark-plural-next-line + return t('sidebar.download.filesize.byte', '{{count}} Byte', { count }) + case 1: + return t('sidebar.download.filesize.kilo_byte', '{{count}} KB', { count }) + case 2: + return t('sidebar.download.filesize.mega_byte', '{{count}} MB', { count }) + case 3: + return t('sidebar.download.filesize.giga_byte', '{{count}} GB', { count }) + case 4: + return t('sidebar.download.filesize.tera_byte', '{{count}} TB', { count }) + default: + throw new Error(`invalid byte value: ${bytes}`) + } +} + +const downloadMedia = (t: TranslationFn) => async (url: string) => { + const imgUrl = new URL( + `${import.meta.env.BASE_URL}${url}`.replace(/\/\//g, '/'), + location.origin + ) + + if (authToken() == null) { + // Get share token if not authorized + const token = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/) + if (token) { + imgUrl.searchParams.set('token', token[1]) + } + } + + const response = await fetch(imgUrl.href, { + credentials: 'include', + }) + + let blob = null + if (response.headers.has('content-length')) { + blob = await downloadMediaShowProgress(t)(response) + } else { + blob = await response.blob() + } + + if (blob == null) { + console.log('Blob is null canceling') + return + } + + const filenameMatch = url.match(/[^/]*$/) + + if (filenameMatch == null) { + console.error('Could not extract filename', url) + return + } + + const filename = filenameMatch[0] + downloadBlob(blob, filename) +} + +const downloadMediaShowProgress = + (t: TranslationFn) => async (response: Response) => { + const totalBytes = Number(response.headers.get('content-length')) + const reader = response.body?.getReader() + const data = new Uint8Array(totalBytes) + + if (reader == null) { + throw new Error('Download reader is null') + } + + let canceled = false + const onDismiss = () => { + canceled = true + reader.cancel('Download canceled by user') + } + + const notifyKey = Math.random().toString(26) + MessageState.add({ + key: notifyKey, + type: NotificationType.Progress, + onDismiss, + props: { + header: 'Downloading photo', + content: `Starting download`, + percent: 0, + }, + }) + + let receivedBytes = 0 + let result + do { + result = await reader.read() + + if (canceled) break + + if (result.value) data.set(result.value, receivedBytes) + + receivedBytes += result.value ? result.value.length : 0 + + MessageState.add({ + key: notifyKey, + type: NotificationType.Progress, + onDismiss, + props: { + header: 'Downloading photo', + percent: (receivedBytes / totalBytes) * 100, + content: `${formatBytes(t)(receivedBytes)} of ${formatBytes(t)( + totalBytes + )} bytes downloaded`, + }, + }) + } while (!result.done) + + if (canceled) { + return + } + + MessageState.add({ + key: notifyKey, + type: NotificationType.Progress, + props: { + header: 'Downloading photo completed', + content: `The photo has been downloaded`, + percent: 100, + positive: true, + }, + }) + + setTimeout(() => { + MessageState.removeKey(notifyKey) + }, 2000) + + const content = new Blob([data.buffer], { + type: response.headers.get('content-type') || undefined, + }) + + return content + } + +const downloadBlob = (blob: Blob, filename: string) => { + const objectUrl = window.URL.createObjectURL(blob) + + const anchor = document.createElement('a') + document.body.appendChild(anchor) + + anchor.href = objectUrl + anchor.download = filename + anchor.click() + + anchor.remove() + + window.URL.revokeObjectURL(objectUrl) +} + +type SidebarDownloadTableRow = { + title: string + url: string + width: number + height: number + fileSize: number +} + +type SidebarDownloadTableProps = { + rows: SidebarDownloadTableRow[] +} + +const SidebarDownloadTable = ({ rows }: SidebarDownloadTableProps) => { + const { t } = useTranslation() + + const extractExtension = (url: string) => { + const urlMatch = url.split(/[#?]/) + if (urlMatch == null) return + + return urlMatch[0].split('.').pop()?.trim().toLowerCase() + } + + const download = downloadMedia(t) + const bytes = formatBytes(t) + const downloadRows = rows.map(x => ( + download(x.url)} tabIndex={0}> + {`${x.title}`} + {`${x.width} x ${x.height}`} + {`${bytes(x.fileSize)}`} + {extractExtension(x.url)} + + )) + + return ( + + + + + {t('sidebar.download.table_columns.name', 'Name')} + + + {t('sidebar.download.table_columns.dimensions', 'Dimensions')} + + + {t('sidebar.download.table_columns.file_size', 'Size')} + + + {t('sidebar.download.table_columns.file_type', 'Type')} + + + + {downloadRows} + + ) +} + +type SidebarMediaDownladProps = { + media: MediaSidebarMedia +} + +const SidebarMediaDownload = ({ media }: SidebarMediaDownladProps) => { + const { t } = useTranslation() + if (!media || !media.id) return null + + const [loadPhotoDownloads, { called, loading, data }] = useLazyQuery< + sidebarDownloadQuery, + sidebarDownloadQueryVariables + >(SIDEBAR_DOWNLOAD_QUERY, { variables: { mediaId: media.id } }) + + let downloads: sidebarDownloadQuery_media_downloads[] = [] + + if (called) { + if (!loading) { + downloads = (data && data.media.downloads) || [] + } + } else { + if (!media.downloads) { + loadPhotoDownloads() + } else { + downloads = media.downloads + } + } + + const downloadRows = downloads.map(x => ({ + title: x.title, + url: x.mediaUrl.url, + width: x.mediaUrl.width, + height: x.mediaUrl.height, + fileSize: x.mediaUrl.fileSize, + })) + + return ( + + + {t('sidebar.download.title', 'Download')} + + + + + ) +} + +export default SidebarMediaDownload diff --git a/ui/src/components/sidebar/SidebarHeader.tsx b/ui/src/components/sidebar/SidebarHeader.tsx new file mode 100644 index 0000000..0a6d39c --- /dev/null +++ b/ui/src/components/sidebar/SidebarHeader.tsx @@ -0,0 +1,38 @@ +import React, { useContext } from 'react' + +import { ReactComponent as CloseIcon } from './icons/closeSidebarIcon.svg' +import { ReactComponent as PinIconOutline } from './icons/pinSidebarIconOutline.svg' +import { ReactComponent as PinIconFilled } from './icons/pinSidebarIconFilled.svg' +import { SidebarContext } from './Sidebar' + +type SidebarHeaderProps = { + title: string +} + +const SidebarHeader = ({ title }: SidebarHeaderProps) => { + const { updateSidebar, setPinned, pinned } = useContext(SidebarContext) + + const PinIcon = pinned ? PinIconFilled : PinIconOutline + + return ( +
    + + {title} + +
    + ) +} + +export default SidebarHeader diff --git a/ui/src/components/sidebar/SidebarItem.tsx b/ui/src/components/sidebar/SidebarItem.tsx new file mode 100644 index 0000000..5cd82b7 --- /dev/null +++ b/ui/src/components/sidebar/SidebarItem.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +type SidebarItemProps = { + name: string + value: string +} + +const SidebarItem = ({ name, value }: SidebarItemProps) => ( +
    +
    + {name} +
    +
    + {value} +
    +
    +) + +export default SidebarItem diff --git a/ui/src/components/sidebar/SidebarTable.tsx b/ui/src/components/sidebar/SidebarTable.tsx new file mode 100644 index 0000000..e05457e --- /dev/null +++ b/ui/src/components/sidebar/SidebarTable.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components' + +const Table = styled.table.attrs({ className: 'table-fixed w-full' })`` + +const Head = styled.thead.attrs({ + className: 'bg-[#f9f9fb] dark:bg-[#2B3037]', +})`` + +const HeadRow = styled.tr.attrs({ + className: + 'text-left uppercase text-xs border-gray-100 dark:border-dark-border2 border-b border-t', +})`` + +const Row = styled.tr.attrs({ + className: + 'cursor-pointer border-gray-100 dark:border-dark-border2 border-b hover:bg-gray-50 focus:bg-gray-50 dark:hover:bg-[#3c4759] dark:focus:bg-[#3c4759]', +})`` + +export default { + Table, + Head, + HeadRow, + Row, +} diff --git a/ui/src/components/sidebar/__generated__/getAlbumSidebar.ts b/ui/src/components/sidebar/__generated__/getAlbumSidebar.ts new file mode 100644 index 0000000..e4e6604 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/getAlbumSidebar.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: getAlbumSidebar +// ==================================================== + +export interface getAlbumSidebar_album { + __typename: "Album"; + id: string; + title: string; +} + +export interface getAlbumSidebar { + /** + * Get album by id, user must own the album or be admin + * If valid tokenCredentials are provided, the album may be retrived without further authentication + */ + album: getAlbumSidebar_album; +} + +export interface getAlbumSidebarVariables { + id: string; +} diff --git a/ui/src/components/sidebar/__generated__/resetAlbumCover.ts b/ui/src/components/sidebar/__generated__/resetAlbumCover.ts new file mode 100644 index 0000000..1b71c76 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/resetAlbumCover.ts @@ -0,0 +1,45 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: resetAlbumCover +// ==================================================== + +export interface resetAlbumCover_resetAlbumCover_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface resetAlbumCover_resetAlbumCover_thumbnail { + __typename: "Media"; + id: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: resetAlbumCover_resetAlbumCover_thumbnail_thumbnail | null; +} + +export interface resetAlbumCover_resetAlbumCover { + __typename: "Album"; + id: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: resetAlbumCover_resetAlbumCover_thumbnail | null; +} + +export interface resetAlbumCover { + /** + * Reset the assigned cover photo for an album + */ + resetAlbumCover: resetAlbumCover_resetAlbumCover; +} + +export interface resetAlbumCoverVariables { + albumID: string; +} diff --git a/ui/src/components/sidebar/__generated__/setAlbumCover.ts b/ui/src/components/sidebar/__generated__/setAlbumCover.ts new file mode 100644 index 0000000..b0b70a0 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/setAlbumCover.ts @@ -0,0 +1,45 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: setAlbumCover +// ==================================================== + +export interface setAlbumCover_setAlbumCover_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface setAlbumCover_setAlbumCover_thumbnail { + __typename: "Media"; + id: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: setAlbumCover_setAlbumCover_thumbnail_thumbnail | null; +} + +export interface setAlbumCover_setAlbumCover { + __typename: "Album"; + id: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: setAlbumCover_setAlbumCover_thumbnail | null; +} + +export interface setAlbumCover { + /** + * Assign a cover photo to an album + */ + setAlbumCover: setAlbumCover_setAlbumCover; +} + +export interface setAlbumCoverVariables { + coverID: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarAlbumAddShare.ts b/ui/src/components/sidebar/__generated__/sidebarAlbumAddShare.ts new file mode 100644 index 0000000..ec3c31c --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarAlbumAddShare.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebarAlbumAddShare +// ==================================================== + +export interface sidebarAlbumAddShare_shareAlbum { + __typename: "ShareToken"; + token: string; +} + +export interface sidebarAlbumAddShare { + /** + * Generate share token for album + */ + shareAlbum: sidebarAlbumAddShare_shareAlbum; +} + +export interface sidebarAlbumAddShareVariables { + id: string; + password?: string | null; + expire?: Time | null; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarDownloadQuery.ts b/ui/src/components/sidebar/__generated__/sidebarDownloadQuery.ts new file mode 100644 index 0000000..59437b1 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarDownloadQuery.ts @@ -0,0 +1,58 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: sidebarDownloadQuery +// ==================================================== + +export interface sidebarDownloadQuery_media_downloads_mediaUrl { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; + /** + * The file size of the resource in bytes + */ + fileSize: number; +} + +export interface sidebarDownloadQuery_media_downloads { + __typename: "MediaDownload"; + /** + * A description of the role of the media file + */ + title: string; + mediaUrl: sidebarDownloadQuery_media_downloads_mediaUrl; +} + +export interface sidebarDownloadQuery_media { + __typename: "Media"; + id: string; + /** + * A list of different versions of files for this media that can be downloaded by the user + */ + downloads: sidebarDownloadQuery_media_downloads[]; +} + +export interface sidebarDownloadQuery { + /** + * Get media by id, user must own the media or be admin. + * If valid tokenCredentials are provided, the media may be retrived without further authentication + */ + media: sidebarDownloadQuery_media; +} + +export interface sidebarDownloadQueryVariables { + mediaId: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarGetAlbumShares.ts b/ui/src/components/sidebar/__generated__/sidebarGetAlbumShares.ts new file mode 100644 index 0000000..e343dc7 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarGetAlbumShares.ts @@ -0,0 +1,43 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: sidebarGetAlbumShares +// ==================================================== + +export interface sidebarGetAlbumShares_album_shares { + __typename: "ShareToken"; + id: string; + token: string; + /** + * Whether or not a password is needed to access the share + */ + hasPassword: boolean; + /** + * Optional expire date + */ + expire: Time | null; +} + +export interface sidebarGetAlbumShares_album { + __typename: "Album"; + id: string; + /** + * A list of share tokens pointing to this album, owned by the logged in user + */ + shares: sidebarGetAlbumShares_album_shares[]; +} + +export interface sidebarGetAlbumShares { + /** + * Get album by id, user must own the album or be admin + * If valid tokenCredentials are provided, the album may be retrived without further authentication + */ + album: sidebarGetAlbumShares_album; +} + +export interface sidebarGetAlbumSharesVariables { + id: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarGetPhotoShares.ts b/ui/src/components/sidebar/__generated__/sidebarGetPhotoShares.ts new file mode 100644 index 0000000..963914f --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarGetPhotoShares.ts @@ -0,0 +1,43 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: sidebarGetPhotoShares +// ==================================================== + +export interface sidebarGetPhotoShares_media_shares { + __typename: "ShareToken"; + id: string; + token: string; + /** + * Whether or not a password is needed to access the share + */ + hasPassword: boolean; + /** + * Optional expire date + */ + expire: Time | null; +} + +export interface sidebarGetPhotoShares_media { + __typename: "Media"; + id: string; + /** + * A list of share tokens pointing to this media, owned byt the logged in user + */ + shares: sidebarGetPhotoShares_media_shares[]; +} + +export interface sidebarGetPhotoShares { + /** + * Get media by id, user must own the media or be admin. + * If valid tokenCredentials are provided, the media may be retrived without further authentication + */ + media: sidebarGetPhotoShares_media; +} + +export interface sidebarGetPhotoSharesVariables { + id: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarPhotoAddShare.ts b/ui/src/components/sidebar/__generated__/sidebarPhotoAddShare.ts new file mode 100644 index 0000000..32038f4 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarPhotoAddShare.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebarPhotoAddShare +// ==================================================== + +export interface sidebarPhotoAddShare_shareMedia { + __typename: "ShareToken"; + token: string; +} + +export interface sidebarPhotoAddShare { + /** + * Generate share token for media + */ + shareMedia: sidebarPhotoAddShare_shareMedia; +} + +export interface sidebarPhotoAddShareVariables { + id: string; + password?: string | null; + expire?: Time | null; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarProtectShare.ts b/ui/src/components/sidebar/__generated__/sidebarProtectShare.ts new file mode 100644 index 0000000..0e889be --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarProtectShare.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebarProtectShare +// ==================================================== + +export interface sidebarProtectShare_protectShareToken { + __typename: "ShareToken"; + token: string; + /** + * Whether or not a password is needed to access the share + */ + hasPassword: boolean; +} + +export interface sidebarProtectShare { + /** + * Set a password for a token, if null is passed for the password argument, the password will be cleared + */ + protectShareToken: sidebarProtectShare_protectShareToken; +} + +export interface sidebarProtectShareVariables { + token: string; + password?: string | null; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarSetExpierShare.ts b/ui/src/components/sidebar/__generated__/sidebarSetExpierShare.ts new file mode 100644 index 0000000..fbb6e4d --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarSetExpierShare.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebarSetExpierShare +// ==================================================== + +export interface sidebarSetExpierShare_setExpireShareToken { + __typename: "ShareToken"; + token: string; +} + +export interface sidebarSetExpierShare { + /** + * Set a Expiration Time for a token + */ + setExpireShareToken: sidebarSetExpierShare_setExpireShareToken; +} + +export interface sidebarSetExpierShareVariables { + token: string; + expire?: Time | null; +} diff --git a/ui/src/components/sidebar/__generated__/sidebareDeleteShare.ts b/ui/src/components/sidebar/__generated__/sidebareDeleteShare.ts new file mode 100644 index 0000000..65eb67e --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebareDeleteShare.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebareDeleteShare +// ==================================================== + +export interface sidebareDeleteShare_deleteShareToken { + __typename: "ShareToken"; + token: string; +} + +export interface sidebareDeleteShare { + /** + * Delete a share token by it's token value + */ + deleteShareToken: sidebareDeleteShare_deleteShareToken; +} + +export interface sidebareDeleteShareVariables { + token: string; +} diff --git a/ui/src/components/sidebar/icons/closeSidebarIcon.svg b/ui/src/components/sidebar/icons/closeSidebarIcon.svg new file mode 100644 index 0000000..18eefbb --- /dev/null +++ b/ui/src/components/sidebar/icons/closeSidebarIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/icons/pinSidebarIconFilled.svg b/ui/src/components/sidebar/icons/pinSidebarIconFilled.svg new file mode 100644 index 0000000..0abac82 --- /dev/null +++ b/ui/src/components/sidebar/icons/pinSidebarIconFilled.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/icons/pinSidebarIconOutline.svg b/ui/src/components/sidebar/icons/pinSidebarIconOutline.svg new file mode 100644 index 0000000..2ba7e90 --- /dev/null +++ b/ui/src/components/sidebar/icons/pinSidebarIconOutline.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/icons/shareAddIcon.svg b/ui/src/components/sidebar/icons/shareAddIcon.svg new file mode 100644 index 0000000..f1f783a --- /dev/null +++ b/ui/src/components/sidebar/icons/shareAddIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/icons/shareCopyIcon.svg b/ui/src/components/sidebar/icons/shareCopyIcon.svg new file mode 100644 index 0000000..b36387b --- /dev/null +++ b/ui/src/components/sidebar/icons/shareCopyIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/icons/shareDeleteIcon.svg b/ui/src/components/sidebar/icons/shareDeleteIcon.svg new file mode 100644 index 0000000..7fc2b8d --- /dev/null +++ b/ui/src/components/sidebar/icons/shareDeleteIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/icons/shareLinkIcon.svg b/ui/src/components/sidebar/icons/shareLinkIcon.svg new file mode 100644 index 0000000..92932b2 --- /dev/null +++ b/ui/src/components/sidebar/icons/shareLinkIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/sidebar/icons/shareMoreIcon.svg b/ui/src/components/sidebar/icons/shareMoreIcon.svg new file mode 100644 index 0000000..6b426eb --- /dev/null +++ b/ui/src/components/sidebar/icons/shareMoreIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/timelineGallery/TimelineFilters.tsx b/ui/src/components/timelineGallery/TimelineFilters.tsx new file mode 100644 index 0000000..d0c4219 --- /dev/null +++ b/ui/src/components/timelineGallery/TimelineFilters.tsx @@ -0,0 +1,108 @@ +import { useQuery } from '@apollo/client' +import gql from 'graphql-tag' +import React from 'react' +import { useTranslation } from 'react-i18next' +import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown' +import { FavoriteCheckboxProps, FavoritesCheckbox } from '../album/AlbumFilter' + +import { ReactComponent as DateIcon } from './icons/date.svg' +import { earliestMedia } from './__generated__/earliestMedia' + +const EARLIEST_MEDIA_QUERY = gql` + query earliestMedia { + myMedia( + order: { order_by: "date_shot", order_direction: ASC } + paginate: { limit: 1 } + ) { + id + date + } + } +` + +type DateSelectorProps = { + filterDate: string | null + setFilterDate(date: string | null): void +} + +const DateSelector = ({ filterDate, setFilterDate }: DateSelectorProps) => { + const { t } = useTranslation() + + const { data, loading } = useQuery(EARLIEST_MEDIA_QUERY) + + let items: DropdownItem[] = [ + { + value: 'all', + label: t('timeline_filter.date.dropdown_all', 'From today'), + }, + ] + + if (data) { + if (data.myMedia.length != 0) { + const dateStr = data.myMedia[0].date + const date = new Date(dateStr) + const now = new Date() + + const currentYear = now.getFullYear() + const earliestYear = date.getFullYear() + + const years: number[] = [] + for (let i = currentYear - 1; i >= earliestYear; i--) { + years.push(i) + } + + const yearItems = years.map(x => ({ + value: `${x}`, + label: t('timeline_filter.date.dropdown_year', '{{year}} and earlier', { + year: x, + }), + })) + items = [...items, ...yearItems] + } + } + + return ( +
    + + +
    + + date == 'all' ? setFilterDate(null) : setFilterDate(date) + } + value={filterDate || 'all'} + items={items} + disabled={loading} + /> +
    +
    + ) +} + +type TimelineFiltersProps = DateSelectorProps & FavoriteCheckboxProps + +const TimelineFilters = ({ + onlyFavorites, + setOnlyFavorites, + filterDate, + setFilterDate, +}: TimelineFiltersProps) => { + return null + // return ( + //
    + // + // + //
    + // ) +} + +export default TimelineFilters diff --git a/ui/src/components/timelineGallery/TimelineGallery.test.tsx b/ui/src/components/timelineGallery/TimelineGallery.test.tsx new file mode 100644 index 0000000..422ec99 --- /dev/null +++ b/ui/src/components/timelineGallery/TimelineGallery.test.tsx @@ -0,0 +1,37 @@ +import { MockedProvider } from '@apollo/client/testing' +import { render, screen } from '@testing-library/react' +import React from 'react' +import { MemoryRouter } from 'react-router-dom' +import TimelineGallery, { MY_TIMELINE_QUERY } from './TimelineGallery' +import { timelineData } from './timelineTestData' + +vi.mock('../../hooks/useScrollPagination') + +test('timeline with media', async () => { + const graphqlMocks = [ + { + request: { + query: MY_TIMELINE_QUERY, + variables: { onlyFavorites: false, offset: 0, limit: 200 }, + }, + result: { + data: { + myTimeline: timelineData, + }, + }, + }, + ] + + render( + + + + + + ) + + expect(screen.queryByLabelText('Show only favorites')).toBeInTheDocument() + + expect(await screen.findAllByRole('link')).toHaveLength(4) + expect(await screen.findAllByRole('img')).toHaveLength(5) +}) diff --git a/ui/src/components/timelineGallery/TimelineGallery.tsx b/ui/src/components/timelineGallery/TimelineGallery.tsx new file mode 100644 index 0000000..10a4337 --- /dev/null +++ b/ui/src/components/timelineGallery/TimelineGallery.tsx @@ -0,0 +1,188 @@ +import React, { useRef, useEffect, useReducer } from 'react' +import { useQuery, gql } from '@apollo/client' +import TimelineGroupDate from './TimelineGroupDate' +import PresentView from '../photoGallery/presentView/PresentView' +import useURLParameters from '../../hooks/useURLParameters' +import useScrollPagination from '../../hooks/useScrollPagination' +import PaginateLoader from '../PaginateLoader' +import { useTranslation } from 'react-i18next' +import { + myTimeline, + myTimelineVariables, + myTimeline_myTimeline, +} from './__generated__/myTimeline' +import { + getActiveTimelineImage as getActiveTimelineMedia, + timelineGalleryReducer, +} from './timelineGalleryReducer' +import { urlPresentModeSetupHook } from '../photoGallery/mediaGalleryReducer' +import TimelineFilters from './TimelineFilters' +import client from '../../apolloClient' + +export const MY_TIMELINE_QUERY = gql` + query myTimeline( + $onlyFavorites: Boolean + $limit: Int + $offset: Int + $fromDate: Time + ) { + myTimeline( + onlyFavorites: $onlyFavorites + fromDate: $fromDate + paginate: { limit: $limit, offset: $offset } + ) { + id + title + type + blurhash + thumbnail { + url + width + height + } + highRes { + url + width + height + } + videoWeb { + url + } + favorite + album { + id + title + } + date + } + } +` + +export type TimelineGroup = { + date: string + albums: TimelineGroupAlbum[] +} + +export type TimelineGroupAlbum = { + id: string + title: string + media: myTimeline_myTimeline[] +} + +const TimelineGallery = () => { + const { t } = useTranslation() + + const { getParam, setParam } = useURLParameters() + + const onlyFavorites = getParam('favorites') == '1' ? true : false + const setOnlyFavorites = (favorites: boolean) => + setParam('favorites', favorites ? '1' : null) + + const filterDate = getParam('date') + const setFilterDate = (x: string) => setParam('date', x) + + const favoritesNeedsRefresh = useRef(false) + + const [mediaState, dispatchMedia] = useReducer(timelineGalleryReducer, { + presenting: false, + timelineGroups: [], + activeIndex: { + date: -1, + album: -1, + media: -1, + }, + }) + + const { data, error, loading, refetch, fetchMore } = useQuery< + myTimeline, + myTimelineVariables + >(MY_TIMELINE_QUERY, { + variables: { + onlyFavorites, + fromDate: filterDate + ? `${parseInt(filterDate) + 1}-01-01T00:00:00Z` + : undefined, + offset: 0, + limit: 200, + }, + }) + + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.myTimeline, + }) + + useEffect(() => { + dispatchMedia({ + type: 'replaceTimelineGroups', + timeline: data?.myTimeline || [], + }) + }, [data]) + + useEffect(() => { + ; (async () => { + await client.resetStore() + await refetch({ + onlyFavorites, + fromDate: filterDate + ? `${parseInt(filterDate) + 1}-01-01T00:00:00Z` + : undefined, + offset: 0, + limit: 200, + }) + })() + }, [filterDate]) + +// urlPresentModeSetupHook({ +// dispatchMedia, +// openPresentMode: (_event) => { +// dispatchMedia({ +// type: 'openPresentMode', +// activeIndex: mediaState.activeIndex, +// }) +// }, +// }) + + useEffect(() => { + favoritesNeedsRefresh.current = false + refetch({ + onlyFavorites: onlyFavorites, + }) + }, [onlyFavorites]) + + if (error) { + return
    {error.message}
    + } + + const timelineGroups = mediaState.timelineGroups.map((_, i) => ( + + )) + + return ( +
    + +
    + {timelineGroups} +
    + +
    + ) +} + +export default TimelineGallery diff --git a/ui/src/components/timelineGallery/TimelineGroupAlbum.tsx b/ui/src/components/timelineGallery/TimelineGroupAlbum.tsx new file mode 100644 index 0000000..89aee8b --- /dev/null +++ b/ui/src/components/timelineGallery/TimelineGroupAlbum.tsx @@ -0,0 +1,92 @@ +import React, { useContext } from 'react' +import { Link } from 'react-router-dom' +import { MediaThumbnail } from '../photoGallery/MediaThumbnail' +import { PhotoFiller } from '../photoGallery/MediaGallery' +import { + toggleFavoriteAction, + useMarkFavoriteMutation, +} from '../photoGallery/photoGalleryMutations' +import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar' +import { SidebarContext } from '../sidebar/Sidebar' +import { + getActiveTimelineImage, + openTimelinePresentMode, + TimelineGalleryAction, + TimelineGalleryState, +} from './timelineGalleryReducer' + +type TimelineGroupAlbumProps = { + dateIndex: number + albumIndex: number + mediaState: TimelineGalleryState + dispatchMedia: React.Dispatch +} + +const TimelineGroupAlbum = ({ + dateIndex, + albumIndex, + mediaState, + dispatchMedia, +}: TimelineGroupAlbumProps) => { + const { + media, + title: albumTitle, + id: albumID, + } = mediaState.timelineGroups[dateIndex].albums[albumIndex] + + const [markFavorite] = useMarkFavoriteMutation() + + const { updateSidebar } = useContext(SidebarContext) + + const mediaElms = media.map((media, index) => ( + { + dispatchMedia({ + type: 'selectImage', + index: { + album: albumIndex, + date: dateIndex, + media: index, + }, + }) + updateSidebar() + }} + clickPresent={() => { + dispatchMedia({ + type: 'selectImage', + index: { + album: albumIndex, + date: dateIndex, + media: index, + }, + }) + updateSidebar() + }} + clickFavorite={() => { + toggleFavoriteAction({ + media, + markFavorite, + }) + }} + active={media.id === getActiveTimelineImage({ mediaState })?.id} + /> + )) + + return ( +
    + + + {albumTitle} + + +
    + {mediaElms} + +
    +
    + ) +} + +export default TimelineGroupAlbum diff --git a/ui/src/components/timelineGallery/TimelineGroupDate.tsx b/ui/src/components/timelineGallery/TimelineGroupDate.tsx new file mode 100644 index 0000000..658ec83 --- /dev/null +++ b/ui/src/components/timelineGallery/TimelineGroupDate.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import TimelineGroupAlbum from './TimelineGroupAlbum' +import { useTranslation } from 'react-i18next' +import { + TimelineGalleryAction, + TimelineGalleryState, +} from './timelineGalleryReducer' + +const dateFormatterOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', +} + +type TimelineGroupDateProps = { + groupIndex: number + mediaState: TimelineGalleryState + dispatchMedia: React.Dispatch +} + +const TimelineGroupDate = ({ + groupIndex, + mediaState, + dispatchMedia, +}: TimelineGroupDateProps) => { + const { i18n } = useTranslation() + + const group = mediaState.timelineGroups[groupIndex] + + const albumGroupElms = group.albums.map((album, i) => ( + + )) + + const dateFormatter = new Intl.DateTimeFormat( + i18n.language, + dateFormatterOptions + ) + + const formattedDate = dateFormatter.format(new Date(group.date)) + + return ( +
    +
    {formattedDate}
    +
    {albumGroupElms}
    +
    + ) +} + +export default TimelineGroupDate diff --git a/ui/src/components/timelineGallery/__generated__/earliestMedia.ts b/ui/src/components/timelineGallery/__generated__/earliestMedia.ts new file mode 100644 index 0000000..8d7b652 --- /dev/null +++ b/ui/src/components/timelineGallery/__generated__/earliestMedia.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: earliestMedia +// ==================================================== + +export interface earliestMedia_myMedia { + __typename: "Media"; + id: string; + /** + * The date the image was shot or the date it was imported as a fallback + */ + date: Time; +} + +export interface earliestMedia { + /** + * List of media owned by the logged in user + */ + myMedia: earliestMedia_myMedia[]; +} diff --git a/ui/src/components/timelineGallery/__generated__/myTimeline.ts b/ui/src/components/timelineGallery/__generated__/myTimeline.ts new file mode 100644 index 0000000..d10a81c --- /dev/null +++ b/ui/src/components/timelineGallery/__generated__/myTimeline.ts @@ -0,0 +1,102 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: myTimeline +// ==================================================== + +export interface myTimeline_myTimeline_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface myTimeline_myTimeline_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; + /** + * Width of the image in pixels + */ + width: number; + /** + * Height of the image in pixels + */ + height: number; +} + +export interface myTimeline_myTimeline_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface myTimeline_myTimeline_album { + __typename: "Album"; + id: string; + title: string; +} + +export interface myTimeline_myTimeline { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * A short string that can be used to generate a blured version of the media, to show while the original is loading + */ + blurhash: string | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: myTimeline_myTimeline_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: myTimeline_myTimeline_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: myTimeline_myTimeline_videoWeb | null; + favorite: boolean; + /** + * The album that holds the media + */ + album: myTimeline_myTimeline_album; + /** + * The date the image was shot or the date it was imported as a fallback + */ + date: Time; +} + +export interface myTimeline { + /** + * Get a list of media, ordered first by day, then by album if multiple media was found for the same day. + */ + myTimeline: myTimeline_myTimeline[]; +} + +export interface myTimelineVariables { + onlyFavorites?: boolean | null; + limit?: number | null; + offset?: number | null; + fromDate?: Time | null; +} diff --git a/ui/src/components/timelineGallery/icons/date.svg b/ui/src/components/timelineGallery/icons/date.svg new file mode 100644 index 0000000..b2da354 --- /dev/null +++ b/ui/src/components/timelineGallery/icons/date.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ui/src/components/timelineGallery/timelineGalleryReducer.test.ts b/ui/src/components/timelineGallery/timelineGalleryReducer.test.ts new file mode 100644 index 0000000..df82b41 --- /dev/null +++ b/ui/src/components/timelineGallery/timelineGalleryReducer.test.ts @@ -0,0 +1,434 @@ +import { MediaType } from '../../__generated__/globalTypes' +import { + timelineGalleryReducer, + TimelineGalleryState, + TimelineMediaIndex, +} from './timelineGalleryReducer' +import { timelineData } from './timelineTestData' + +describe('timeline gallery reducer', () => { + const defaultEmptyState: TimelineGalleryState = { + presenting: false, + activeIndex: { + album: -1, + date: -1, + media: -1, + }, + timelineGroups: [], + } + + const defaultState = timelineGalleryReducer(defaultEmptyState, { + type: 'replaceTimelineGroups', + timeline: timelineData, + }) + + test('replace timeline groups', () => { + expect(defaultState).toMatchObject({ + presenting: false, + activeIndex: { + album: -1, + date: -1, + media: -1, + }, + timelineGroups: [ + { + date: '2020-12-13T00:00:00Z', + albums: [ + { + id: '522', + media: [ + { + __typename: 'Media', + album: { + __typename: 'Album', + id: '522', + title: 'random', + }, + date: '2020-12-13T18:03:40Z', + favorite: false, + highRes: { + __typename: 'MediaURL', + height: 4480, + url: '/photo/122A2876_5cSPMiKL.jpg', + width: 6720, + }, + id: '1058', + thumbnail: { + __typename: 'MediaURL', + height: 682, + url: '/photo/thumbnail_122A2876_jpg_Kp1U80vD.jpg', + width: 1024, + }, + title: '122A2876.jpg', + type: 'Photo', + videoWeb: null, + }, + ], + title: 'random', + }, + ], + }, + { + date: '2020-11-25T00:00:00Z', + albums: [ + { + id: '523', + title: 'another_album', + media: [ + { + __typename: 'Media', + album: { + __typename: 'Album', + id: '523', + title: 'another_album', + }, + date: '2020-11-25T16:14:33Z', + favorite: false, + highRes: { + __typename: 'MediaURL', + height: 4118, + url: '/photo/122A2630-Edit_ySQWFAgE.jpg', + width: 6177, + }, + id: '1059', + thumbnail: { + __typename: 'MediaURL', + height: 682, + url: '/photo/thumbnail_122A2630-Edit_jpg_pwjtMkpy.jpg', + width: 1024, + }, + title: '122A2630-Edit.jpg', + type: 'Photo', + videoWeb: null, + }, + { + __typename: 'Media', + album: { + __typename: 'Album', + id: '523', + title: 'another_album', + }, + date: '2020-11-25T16:43:59Z', + favorite: false, + highRes: { + __typename: 'MediaURL', + height: 884, + url: '/photo/122A2785-2_mCnWjLdb.jpg', + width: 884, + }, + id: '1060', + thumbnail: { + __typename: 'MediaURL', + height: 1024, + url: '/photo/thumbnail_122A2785-2_jpg_CevmxEXf.jpg', + width: 1024, + }, + title: '122A2785-2.jpg', + type: 'Photo', + videoWeb: null, + }, + ], + }, + { + id: '522', + title: 'random', + media: [ + { + __typename: 'Media', + album: { + __typename: 'Album', + id: '522', + title: 'random', + }, + date: '2020-11-25T16:14:33Z', + favorite: false, + highRes: { + __typename: 'MediaURL', + height: 4118, + url: '/photo/122A2630-Edit_em9g89qg.jpg', + width: 6177, + }, + id: '1056', + thumbnail: { + __typename: 'MediaURL', + height: 682, + url: '/photo/thumbnail_122A2630-Edit_jpg_aJPCSDDl.jpg', + width: 1024, + }, + title: '122A2630-Edit.jpg', + type: 'Photo', + videoWeb: null, + }, + ], + }, + ], + }, + { + date: '2020-11-09T00:00:00Z', + albums: [ + { + id: '522', + title: 'random', + media: [ + { + __typename: 'Media', + id: '1054', + title: '122A2559.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_122A2559_jpg_MsOJtPi8.jpg', + width: 1024, + height: 712, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/122A2559_FDsQHuBN.jpg', + width: 6246, + height: 4346, + }, + videoWeb: null, + favorite: false, + album: { __typename: 'Album', id: '522', title: 'random' }, + date: '2020-11-09T15:38:09Z', + }, + ], + }, + ], + }, + ], + }) + }) + + test('select image', () => { + expect( + timelineGalleryReducer(defaultState, { + type: 'selectImage', + index: { + album: 0, + date: 0, + media: 0, + }, + }) + ).toEqual({ + ...defaultState, + activeIndex: { + album: 0, + date: 0, + media: 0, + }, + }) + }) + + describe('next image', () => { + const testIndexes: { + name: string + in: TimelineMediaIndex + out: TimelineMediaIndex + }[] = [ + { + name: 'no selection', + in: { + date: -1, + album: -1, + media: -1, + }, + out: { + date: -1, + album: -1, + media: -1, + }, + }, + { + name: 'first selected', + in: { + date: 0, + album: 0, + media: 0, + }, + out: { + date: 1, + album: 0, + media: 0, + }, + }, + { + name: 'next album', + in: { + date: 1, + album: 0, + media: 1, + }, + out: { + date: 1, + album: 1, + media: 0, + }, + }, + { + name: 'next date', + in: { + date: 1, + album: 1, + media: 0, + }, + out: { + date: 2, + album: 0, + media: 0, + }, + }, + { + name: 'reached end', + in: { + date: 2, + album: 0, + media: 0, + }, + out: { + date: 2, + album: 0, + media: 0, + }, + }, + ] + + testIndexes.forEach(t => { + test(t.name, () => { + expect( + timelineGalleryReducer( + { + ...defaultState, + activeIndex: t.in, + }, + { type: 'nextImage' } + ) + ).toEqual({ + ...defaultState, + activeIndex: t.out, + }) + }) + }) + }) + + describe('previous image', () => { + const testIndexes: { + name: string + in: TimelineMediaIndex + out: TimelineMediaIndex + }[] = [ + { + name: 'no selection', + in: { + album: -1, + date: -1, + media: -1, + }, + out: { + album: -1, + date: -1, + media: -1, + }, + }, + { + name: 'first selected', + in: { + album: 0, + date: 0, + media: 0, + }, + out: { + album: 0, + date: 0, + media: 0, + }, + }, + { + name: 'previous album', + in: { + date: 1, + album: 1, + media: 0, + }, + out: { + date: 1, + album: 0, + media: 1, + }, + }, + { + name: 'previous date', + in: { + date: 2, + album: 0, + media: 0, + }, + out: { + date: 1, + album: 1, + media: 0, + }, + }, + { + name: 'previous media', + in: { + date: 0, + album: 0, + media: 1, + }, + out: { + date: 0, + album: 0, + media: 0, + }, + }, + ] + + testIndexes.forEach(t => { + test(t.name, () => { + expect( + timelineGalleryReducer( + { + ...defaultState, + activeIndex: t.in, + }, + { type: 'previousImage' } + ) + ).toEqual({ + ...defaultState, + activeIndex: t.out, + }) + }) + }) + }) + + test('present mode', () => { + expect( + timelineGalleryReducer(defaultState, { + type: 'openPresentMode', + activeIndex: { + date: 1, + album: 0, + media: 0, + }, + }) + ).toEqual({ + ...defaultState, + presenting: true, + activeIndex: { + date: 1, + album: 0, + media: 0, + }, + }) + + expect( + timelineGalleryReducer(defaultState, { + type: 'closePresentMode', + }) + ).toEqual({ + ...defaultState, + presenting: false, + }) + }) +}) diff --git a/ui/src/components/timelineGallery/timelineGalleryReducer.tsx b/ui/src/components/timelineGallery/timelineGalleryReducer.tsx new file mode 100644 index 0000000..9700703 --- /dev/null +++ b/ui/src/components/timelineGallery/timelineGalleryReducer.tsx @@ -0,0 +1,274 @@ +import React from 'react' +import { myTimeline_myTimeline } from './__generated__/myTimeline' +import { TimelineGroup, TimelineGroupAlbum } from './TimelineGallery' +import { GalleryAction } from '../photoGallery/mediaGalleryReducer' +import { isNil } from '../../helpers/utils' + +export interface TimelineMediaIndex { + date: number + album: number + media: number +} + +export interface TimelineGalleryState { + presenting: boolean + timelineGroups: TimelineGroup[] + activeIndex: TimelineMediaIndex +} + +export type TimelineGalleryAction = + | GalleryAction + | { type: 'replaceTimelineGroups'; timeline: myTimeline_myTimeline[] } + | { type: 'selectImage'; index: TimelineMediaIndex } + | { type: 'openPresentMode'; activeIndex: TimelineMediaIndex } + +export function timelineGalleryReducer( + state: TimelineGalleryState, + action: TimelineGalleryAction +): TimelineGalleryState { + switch (action.type) { + case 'replaceTimelineGroups': { + const timelineGroups = convertMediaToTimelineGroups(action.timeline) + + return { + ...state, + activeIndex: { + album: -1, + date: -1, + media: -1, + }, + timelineGroups, + } + } + case 'nextImage': { + const { activeIndex, timelineGroups } = state + + if ( + activeIndex.album == -1 && + activeIndex.date == -1 && + activeIndex.media == -1 + ) { + return state + } + + const albumGroups = timelineGroups[activeIndex.date].albums + const albumMedia = albumGroups[activeIndex.album].media + + if (activeIndex.media < albumMedia.length - 1) { + return { + ...state, + activeIndex: { + ...state.activeIndex, + media: activeIndex.media + 1, + }, + } + } + + if (activeIndex.album < albumGroups.length - 1) { + return { + ...state, + activeIndex: { + ...state.activeIndex, + album: activeIndex.album + 1, + media: 0, + }, + } + } + + if (activeIndex.date < timelineGroups.length - 1) { + return { + ...state, + activeIndex: { + date: activeIndex.date + 1, + album: 0, + media: 0, + }, + } + } + + // reached the end + return state + } + case 'previousImage': { + const { activeIndex } = state + + if ( + activeIndex.album == -1 && + activeIndex.date == -1 && + activeIndex.media == -1 + ) { + return state + } + + if (activeIndex.media > 0) { + return { + ...state, + activeIndex: { + ...activeIndex, + media: activeIndex.media - 1, + }, + } + } + + if (activeIndex.album > 0) { + const albumGroups = state.timelineGroups[activeIndex.date].albums + const albumMedia = albumGroups[activeIndex.album - 1].media + + return { + ...state, + activeIndex: { + ...activeIndex, + album: activeIndex.album - 1, + media: albumMedia.length - 1, + }, + } + } + + if (activeIndex.date > 0) { + const albumGroups = state.timelineGroups[activeIndex.date - 1].albums + const albumMedia = albumGroups[albumGroups.length - 1].media + + return { + ...state, + activeIndex: { + date: activeIndex.date - 1, + album: albumGroups.length - 1, + media: albumMedia.length - 1, + }, + } + } + + // reached the start + return state + } + case 'selectImage': { + return { + ...state, + activeIndex: action.index, + } + } + case 'openPresentMode': + return { + ...state, + presenting: true, + activeIndex: action.activeIndex, + } + case 'closePresentMode': { + return { + ...state, + presenting: false, + } + } + } +} + +export const getTimelineImage = ({ + mediaState, + index, +}: { + mediaState: TimelineGalleryState + index: TimelineMediaIndex +}): myTimeline_myTimeline => { + const { date, album, media } = index + return mediaState.timelineGroups[date].albums[album].media[media] +} + +export const getActiveTimelineImage = ({ + mediaState, +}: { + mediaState: TimelineGalleryState +}) => { + if ( + Object.values(mediaState.activeIndex).reduce( + (acc, next) => next === -1 || acc, + false + ) + ) { + return undefined + } + + return getTimelineImage({ mediaState, index: mediaState.activeIndex }) +} + +function convertMediaToTimelineGroups( + timelineMedia: myTimeline_myTimeline[] +): TimelineGroup[] { + const timelineGroups: TimelineGroup[] = [] + let albums: TimelineGroupAlbum[] = [] + let nextAlbum: TimelineGroupAlbum | null = null + + const sameDay = (a: string, b: string) => { + return ( + a.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00') == + b.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00') + ) + } + + for (const media of timelineMedia) { + if (nextAlbum == null) { + nextAlbum = { + id: media.album.id, + title: media.album.title, + media: [media], + } + continue + } + + // if date changes + if (!sameDay(nextAlbum.media[0].date, media.date)) { + albums.push(nextAlbum) + + timelineGroups.push({ + date: albums[0].media[0].date.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00'), + albums: albums, + }) + albums = [] + nextAlbum = { + id: media.album.id, + title: media.album.title, + media: [media], + } + continue + } + + // if album changes + if (nextAlbum.id != media.album.id) { + albums.push(nextAlbum) + nextAlbum = { + id: media.album.id, + title: media.album.title, + media: [media], + } + continue + } + + // same album and date + nextAlbum.media.push(media) + } + + if (!isNil(nextAlbum)) { + albums.push(nextAlbum) + + timelineGroups.push({ + date: albums[0].media[0].date.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00'), + albums: albums, + }) + } + + return timelineGroups +} + +export const openTimelinePresentMode = ({ + dispatchMedia, + activeIndex, +}: { + dispatchMedia: React.Dispatch + activeIndex: TimelineMediaIndex +}) => { + dispatchMedia({ + type: 'openPresentMode', + activeIndex, + }) + + history.pushState({ presenting: true, activeIndex: activeIndex }, '') +} diff --git a/ui/src/components/timelineGallery/timelineTestData.ts b/ui/src/components/timelineGallery/timelineTestData.ts new file mode 100644 index 0000000..f08d33b --- /dev/null +++ b/ui/src/components/timelineGallery/timelineTestData.ts @@ -0,0 +1,120 @@ +import { MediaType } from '../../__generated__/globalTypes' +import { myTimeline_myTimeline } from './__generated__/myTimeline' + +export const timelineData: myTimeline_myTimeline[] = [ + { + __typename: 'Media', + id: '1058', + title: '122A2876.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_122A2876_jpg_Kp1U80vD.jpg', + width: 1024, + height: 682, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/122A2876_5cSPMiKL.jpg', + width: 6720, + height: 4480, + }, + videoWeb: null, + favorite: false, + album: { __typename: 'Album', id: '522', title: 'random' }, + date: '2020-12-13T18:03:40Z', + blurhash: null, + }, + { + __typename: 'Media', + id: '1059', + title: '122A2630-Edit.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_122A2630-Edit_jpg_pwjtMkpy.jpg', + width: 1024, + height: 682, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/122A2630-Edit_ySQWFAgE.jpg', + width: 6177, + height: 4118, + }, + videoWeb: null, + favorite: false, + album: { __typename: 'Album', id: '523', title: 'another_album' }, + date: '2020-11-25T16:14:33Z', + blurhash: null, + }, + { + __typename: 'Media', + id: '1060', + title: '122A2785-2.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_122A2785-2_jpg_CevmxEXf.jpg', + width: 1024, + height: 1024, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/122A2785-2_mCnWjLdb.jpg', + width: 884, + height: 884, + }, + videoWeb: null, + favorite: false, + album: { __typename: 'Album', id: '523', title: 'another_album' }, + date: '2020-11-25T16:43:59Z', + blurhash: null, + }, + { + __typename: 'Media', + id: '1056', + title: '122A2630-Edit.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_122A2630-Edit_jpg_aJPCSDDl.jpg', + width: 1024, + height: 682, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/122A2630-Edit_em9g89qg.jpg', + width: 6177, + height: 4118, + }, + videoWeb: null, + favorite: false, + album: { __typename: 'Album', id: '522', title: 'random' }, + date: '2020-11-25T16:14:33Z', + blurhash: null, + }, + { + __typename: 'Media', + id: '1054', + title: '122A2559.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: '/photo/thumbnail_122A2559_jpg_MsOJtPi8.jpg', + width: 1024, + height: 712, + }, + highRes: { + __typename: 'MediaURL', + url: '/photo/122A2559_FDsQHuBN.jpg', + width: 6246, + height: 4346, + }, + videoWeb: null, + favorite: false, + album: { __typename: 'Album', id: '522', title: 'random' }, + date: '2020-11-09T15:38:09Z', + blurhash: null, + }, +] diff --git a/ui/src/extractedTranslations/da/translation.json b/ui/src/extractedTranslations/da/translation.json new file mode 100644 index 0000000..ad48f43 --- /dev/null +++ b/ui/src/extractedTranslations/da/translation.json @@ -0,0 +1,371 @@ +{ + "album_filter": { + "only_favorites": "Vis kun favoritter", + "sort": "Sorter", + "sort_by": "Sorter efter", + "sort_direction": "Sorteringsretning", + "order_direction": { + "ascending": "Stigende", + "descending": "Faldende" + }, + "sorting_options": { + "date_imported": "Dato for importering", + "date_shot": "Dato", + "title": "Titel", + "type": "Type" + } + }, + "general": { + "action": { + "add": "Tilføj", + "cancel": "Annuller", + "remove": "Fjern", + "save": "Gem" + }, + "loading": { + "album": "Loader album", + "default": "Loader...", + "page": "Loader side", + "paginate": { + "faces": "Loader flere personer", + "media": "Loader flere medier" + }, + "shares": "Loader delinger..." + } + }, + "header": { + "search": { + "loading": "Loader resultater...", + "no_results": "Fandt ingen resultater", + "placeholder": "Søg", + "result_type": { + "albums": "Albums", + "media": "Medie" + } + } + }, + "login_page": { + "field": { + "password": "Adgangskode", + "submit": "Log ind", + "username": "Brugernavn" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Billedesti", + "placeholder": "/sti/til/billeder" + }, + "submit": "Opsæt Photoview" + }, + "title": "Førstegangsopsætning" + }, + "welcome": "Velkommen til Photoview" + }, + "meta": { + "description": "Simpelt og Brugervenligt Photo-galleri for Personlige Servere" + }, + "people_page": { + "action_label": { + "change_label": "Ændre navn", + "detach_images": "Løsriv billeder", + "merge_face": "", + "merge_people": "Sammenflet personer", + "move_faces": "Flyt ansigter" + }, + "face_group": { + "label_placeholder": "Navn", + "unlabeled": "Ikke navngivet", + "unlabeled_person": "Ikke navngivet person" + }, + "modal": { + "action": { + "merge": "Sammenflet", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "Løsriv billeder", + "select_images": "Vælg billeder som skal løsrives" + }, + "description": "Løsriv valgte billeder fra denne gruppe og flyt dem til sin egen gruppe", + "title": "Løsriv Billeder" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "Vælg destinationsgruppe" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "Vælg gruppe at flette med" + }, + "move_image_faces": { + "description": "Flyt valgte billeder fra denne gruppe til en anden gruppe", + "destination_face_group_table": { + "move_action": "Flyt ansigter", + "title": "Vælg destinationsgruppe" + }, + "image_select_table": { + "next_action": "Næste", + "title": "Vælg billeder som skal flyttes" + }, + "title": "Flyt Ansigter" + } + }, + "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter", + "tableselect_face_group": { + "search_faces_placeholder": "Søg ansigter..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Søg billeder..." + } + }, + "photos_page": { + "title": "Tidslinje" + }, + "places_page": { + "title": "Kort" + }, + "routes": { + "page_not_found": "Side ikke fundet" + }, + "settings": { + "concurrent_workers": { + "description": "Det maksimale antal medier som må skannes samtidig", + "title": "Samtidige scanner-arbejdere" + }, + "logout": "Log ud", + "periodic_scanner": { + "checkbox_label": "Aktiver periodiske scanner", + "field": { + "description": "Hvor ofte scanneren bør udføre automatiske scanninger af alle brugere", + "label": "Periodiske scanningsintervaller" + }, + "interval_unit": { + "days": "Dage", + "hour": "Timer", + "minutes": "Minutter", + "months": "Måneder", + "seconds": "Sekunder" + }, + "title": "Periodisk scanner" + }, + "scanner": { + "description": "Vil scanne alle brugere for nye eller opdaterede medier", + "scan_all_users": "Scan alle brugere", + "title": "Scanner" + }, + "user_preferences": { + "change_language": { + "description": "Set sidens sprog specifikt for denne bruger", + "label": "Sprog" + }, + "language_selector": { + "placeholder": "Vælg sprog" + }, + "theme": { + "auto": { + "label": "Samme som system" + }, + "dark": { + "label": "Mørk" + }, + "description": "Ændre hjemmesidens udseende", + "light": { + "label": "Lys" + }, + "title": "Temaindstillinger" + }, + "title": "Brugerindstillinger" + }, + "users": { + "add_user": { + "submit": "Tilføj bruger" + }, + "confirm_delete_user": { + "action": "Slet {user}", + "description": "<0>Er du sikker på at du vil slette <1>?

    Denne handling kan ikke fortrydes

    ", + "title": "Slet bruger" + }, + "password_reset": { + "description": "Ændre adgangskode for <1>{{username}}", + "form": { + "label": "Ny adgangskode", + "placeholder": "adgangskode", + "submit": "Ændre adgangskode" + }, + "title": "Ændre adgangskode" + }, + "table": { + "column_names": { + "action": "Handling", + "capabilities": "Rettigheder", + "photo_path": "Billedesti", + "username": "Brugernavn" + }, + "new_user": "Ny bruger", + "row": { + "action": { + "change_password": "Ændre adgangskode", + "delete": "Slet", + "edit": "Rediger", + "scan": "Scan" + } + } + }, + "title": "Brugere" + }, + "version_info": { + "build_date_title": "Dato for udgivelse", + "title": "Photoview Version", + "version_title": "Udgivelsesversion" + } + }, + "share_page": { + "media": { + "title": "Delt medie" + }, + "protected_share": { + "description": "Denne deling er låst med en adgangskode.", + "password_required_error": "Adgangskode er påkrævet", + "title": "Beskyttet deling" + }, + "share_not_found": "Deling blev ikke fundet", + "share_not_found_description": "Måske er delingen udløbet eller blevet slettet.", + "wrong_password": "Forkert adgangskode, prøv venligst igen." + }, + "sidebar": { + "album": { + "album_cover": "Album coverbillede", + "download": { + "high-resolutions": { + "description": "Høj opløsning JPEGs af RAW-billeder", + "title": "Høj opløsning" + }, + "originals": { + "description": "De originale billeder og video", + "title": "Originaler" + }, + "thumbnails": { + "description": "Billeder i lav opløsning, ingen videoer", + "title": "Thumbnails" + }, + "web-videos": { + "description": "Videoer som er blevet optimeret til web", + "title": "Konverterede videoer" + } + }, + "reset_cover": "Nulstil coverbillede", + "set_cover": "Set som album coverbillede", + "title_placeholder": "Albumtitel" + }, + "download": { + "filesize": { + "byte_one": "", + "byte_other": "", + "giga_byte_one": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Dimension", + "file_size": "Størrelse", + "file_type": "Type", + "name": "Navn" + }, + "title": "Download" + }, + "location": { + "title": "Lokation" + }, + "media": { + "album_path": "Album sti", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Actionprogram", + "aperture_priority": "Blændeprioritet", + "bulb": "Bulb", + "creative_program": "Kreativ program", + "landscape_mode": "Landskabsmode", + "manual": "Manuel", + "normal_program": "Normal program", + "not_defined": "Ikke defineret", + "portrait_mode": "Portræt mode", + "shutter_priority": "Lukkerprioritet" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Blitz affyrede ikke", + "fired": "Affyrede", + "no_flash": "Ingen blitz", + "no_flash_function": "Ingen blitz-funktion", + "off": "Slukket", + "on": "Tændt", + "red_eye_reduction": "Røde øjne reduktion", + "return_detected": "Retur registreret", + "return_not_detected": "Retur ikke registreret" + }, + "name": { + "aperture": "Blænde", + "camera": "Kamera", + "coordinates": "Koordinater", + "date_shot": "Dato", + "exposure": "Lukketid", + "exposure_program": "Lukketid program", + "flash": "Blitz", + "focal_length": "Brændvidde", + "iso": "ISO", + "lens": "Objektiv", + "maker": "Mærke" + } + } + }, + "people": { + "action_label": { + "detach_image": "Løsriv billede", + "merge_face": "Sammenflet person", + "move_face": "Flyt person" + }, + "confirm_image_detach": "Er du sikker på at du vil løsrive dette billede?", + "title": "Personer" + }, + "sharing": { + "add_share": "Tilføj deling", + "copy_link": "Kopier link", + "delete": "Slet", + "more": "Mere", + "no_shares_found": "Ingen delinger fundet", + "public_link": "Offentligt link", + "title": "Indstillinger for deling" + } + }, + "sidemenu": { + "albums": "Albums", + "people": "Personer", + "photos": "Tidslinje", + "places": "Kort", + "settings": "Indstillinger" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Fra i dag", + "dropdown_year": "{{year}} og tidligere", + "label": "Dato" + } + }, + "title": { + "loading_album": "Loader album", + "login": "Log ind", + "people": "Personer", + "settings": "Indstillinger" + } +} diff --git a/ui/src/extractedTranslations/da/translation_old.json b/ui/src/extractedTranslations/da/translation_old.json new file mode 100644 index 0000000..5676905 --- /dev/null +++ b/ui/src/extractedTranslations/da/translation_old.json @@ -0,0 +1,118 @@ +{ + "albums_page": { + "title": "Album" + }, + "general": { + "action": { + "delete": "Slet", + "more": "Mere" + }, + "loading": { + "media": "Loader medier", + "timeline": "Loader tidslinje" + } + }, + "header": { + "search": { + "result_type": { + "photos": "Billeder" + } + } + }, + "people_page": { + "action_label": { + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "face_group": { + "action": { + "add_label": "Tilføj navn", + "change_label": "Ændre navn", + "detach_face": "Løsriv ansigter", + "merge_face": "Sammenflet ansigter", + "move_faces": "Flyt ansigter" + } + }, + "modal": { + "merge_face_groups": { + "description": "Alle billeder fra denne gruppe vil blive flettet sammen med den valgte gruppe" + }, + "merge_people_groups": { + "description": "Alle billeder fra denne gruppe vil blive flettet sammen med den valgte gruppe", + "destination_table": { + "title": "Vælg destinationsgruppe" + }, + "title": "Vælg gruppe at flette med" + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": "Søg ansigter..." + }, + "select_image_faces": { + "search_images_placeholder": "Søg billeder..." + } + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Admin" + } + } + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "Indstillinger for album" + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "" + } + }, + "media": { + "album": "Album" + }, + "sharing": { + "table_header": "Offentlige delinger" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + } +} diff --git a/ui/src/extractedTranslations/de/translation.json b/ui/src/extractedTranslations/de/translation.json new file mode 100644 index 0000000..fe68e6a --- /dev/null +++ b/ui/src/extractedTranslations/de/translation.json @@ -0,0 +1,371 @@ +{ + "album_filter": { + "only_favorites": "Nur Favoriten anzeigen", + "sort": "Sortierung", + "sort_by": "Sortieren nach", + "sort_direction": "Sortierrichtung", + "order_direction": { + "ascending": "Aufsteigend", + "descending": "Absteigend" + }, + "sorting_options": { + "date_imported": "Importdatum", + "date_shot": "Aufnahmedatum", + "title": "Titel", + "type": "Typ" + } + }, + "general": { + "action": { + "add": "Hinzufügen", + "cancel": "Abbrechen", + "remove": "Entfernen", + "save": "Speichern" + }, + "loading": { + "album": "Lade Album", + "default": "Lade...", + "page": "Lade Seite", + "paginate": { + "faces": "Lade mehr Personen", + "media": "Lade mehr Medien" + }, + "shares": "Lade Freigaben..." + } + }, + "header": { + "search": { + "loading": "Lade Ergebnisse...", + "no_results": "Keine Ergebnisse gefunden", + "placeholder": "Suche", + "result_type": { + "albums": "Alben", + "media": "" + } + } + }, + "login_page": { + "field": { + "password": "Passwort", + "submit": "Einloggen", + "username": "Benutzername" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Foto Pfad", + "placeholder": "/pfad/zu/fotos" + }, + "submit": "Photoview einrichten" + }, + "title": "Einrichtung" + }, + "welcome": "Willkommen zu Photoview" + }, + "meta": { + "description": "Einfache und nutzerfreundliche Fotogallerie für Homeserver" + }, + "people_page": { + "action_label": { + "change_label": "Name ändern", + "detach_images": "Bild ablösen", + "merge_face": "", + "merge_people": "Gesichter zusammenführen", + "move_faces": "Gesicht verschieben " + }, + "face_group": { + "label_placeholder": "Zuordnung", + "unlabeled": "Nicht zugeordnet", + "unlabeled_person": "" + }, + "modal": { + "action": { + "merge": "zusammenführen", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "Ablösen", + "select_images": "Bilder auswählen" + }, + "description": "Ausgewählte Bilder werden von dieser Person abgelöst.", + "title": "Bilder von Person ablösen" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "Personen zusammenführen" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "Personen zusammenführen" + }, + "move_image_faces": { + "description": "Verschiebe ausgewählte Bilder in eine andere Person", + "destination_face_group_table": { + "move_action": "Verschiebe", + "title": "Verschiebe Bilder zu Person" + }, + "image_select_table": { + "next_action": "weiter...", + "title": "Verschiebe Bilder zu Person" + }, + "title": "Verschiebe Bilder zu Person" + } + }, + "recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen", + "tableselect_face_group": { + "search_faces_placeholder": "" + }, + "tableselect_image_faces": { + "search_images_placeholder": "" + } + }, + "photos_page": { + "title": "Fotos" + }, + "places_page": { + "title": "Orte" + }, + "routes": { + "page_not_found": "Seite nicht gefunden" + }, + "settings": { + "concurrent_workers": { + "description": "Die maximale erlaubte Anzahl an gleichzeitig laufenden Scanner Jobs", + "title": "Anzahl an Workern" + }, + "logout": "Ausloggen", + "periodic_scanner": { + "checkbox_label": "Automatischen Scanner aktivieren", + "field": { + "description": "Intervall, in dem automatische Scans aller Nutzer wiederholt werden", + "label": "Intervall - Automatischer Scanner" + }, + "interval_unit": { + "days": "Tage", + "hour": "Stunden", + "minutes": "Minuten", + "months": "Monate", + "seconds": "Sekunden" + }, + "title": "Automatischer Scanner" + }, + "scanner": { + "description": "Neue und aktualisierte Medien aller Benutzer scannen", + "scan_all_users": "Alle Benutzer scannen", + "title": "Scanner" + }, + "user_preferences": { + "change_language": { + "description": "Ändert die Spracheinstellungen der Website für diesen Benutzer", + "label": "Sprache" + }, + "language_selector": { + "placeholder": "Sprache auswählen" + }, + "theme": { + "auto": { + "label": "automatisch" + }, + "dark": { + "label": "dunkel" + }, + "description": "Ändert die Darstellung der Seiten", + "light": { + "label": "hell" + }, + "title": "Darstellung Einstellungen" + }, + "title": "Persönliche Einstellungen" + }, + "users": { + "add_user": { + "submit": "Benutzer hinzufügen" + }, + "confirm_delete_user": { + "action": "Lösche {{user}}", + "description": "<0>Du bist dabei, den Nutzer <1> endgültig zu löschen.

    Diese Aktion kann nicht rückgängig gemacht werden. Bist du sicher?

    ", + "title": "Benutzer löschen" + }, + "password_reset": { + "description": "Passwort von diesem Benutzer zurücksetzen.", + "form": { + "label": "Neues Passwort", + "placeholder": "Passwort", + "submit": "Passwort zurücksetzen" + }, + "title": "Passwort zurücksetzen" + }, + "table": { + "column_names": { + "action": "Aktion", + "capabilities": "Berechtigung", + "photo_path": "Speicherort der Medien", + "username": "Benutzername" + }, + "new_user": "Neuer Benutzer", + "row": { + "action": { + "change_password": "Passwort zurücksetzen", + "delete": "Löschen", + "edit": "Bearbeiten", + "scan": "Scannen" + } + } + }, + "title": "Benutzer" + }, + "version_info": { + "build_date_title": "Build-Datum", + "title": "Photoview Version", + "version_title": "Version" + } + }, + "share_page": { + "media": { + "title": "Freigegebene Medien" + }, + "protected_share": { + "description": "Diese Freigabe ist passwortgeschützt.", + "password_required_error": "Password wird benötigt", + "title": "Passwortgeschützte Freigabe" + }, + "share_not_found": "Freigabe nicht gefunden", + "share_not_found_description": "Möglicherweise ist die Freigabe abgelaufen oder wurde gelöscht.", + "wrong_password": "Falsches Passwort, bitte erneut probieren." + }, + "sidebar": { + "album": { + "album_cover": "Album Titelbild", + "download": { + "high-resolutions": { + "description": "hochauflösende Bilder von RAW Dateien", + "title": "Hochauflösend" + }, + "originals": { + "description": "Originale Bilder", + "title": "Originale" + }, + "thumbnails": { + "description": "Geringe Auflösung ohne Videos", + "title": "Miniaturansichten" + }, + "web-videos": { + "description": "Videos, für Web optimiert", + "title": "Weboptimiert" + } + }, + "reset_cover": "Titelbild zurücksetzen", + "set_cover": "Als Album Titelbild setzen", + "title_placeholder": "" + }, + "download": { + "filesize": { + "byte_one": "", + "byte_other": "", + "giga_byte_one": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Auflösung", + "file_size": "Größe", + "file_type": "Typ", + "name": "Name" + }, + "title": "Download" + }, + "location": { + "title": "Karte" + }, + "media": { + "album_path": "Pfad", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Action (kurze Verschlusszeit)", + "aperture_priority": "Blende Priorität", + "bulb": "Bulb", + "creative_program": "Creative (lange Verschlusszeit)", + "landscape_mode": "Querformat", + "manual": "Manuell", + "normal_program": "Normalprogramm", + "not_defined": "Nicht definiert", + "portrait_mode": "Hochformat", + "shutter_priority": "Verschlusszeit Priorität" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Nicht ausgelöst", + "fired": "ausgelöst", + "no_flash": "Kein Blitz", + "no_flash_function": "Keine Blitz Funktionalität", + "off": "Aus", + "on": "An", + "red_eye_reduction": "Rote-Augen-Korrektur", + "return_detected": "Reflektion erkannt", + "return_not_detected": "Reflektion nicht erkannt" + }, + "name": { + "aperture": "Blende", + "camera": "Kamera", + "coordinates": "Koordinaten", + "date_shot": "Aufnahmedatum", + "exposure": "Belichtung", + "exposure_program": "Programm", + "flash": "Blitz", + "focal_length": "Brennweite", + "iso": "ISO", + "lens": "Objektiv", + "maker": "Hersteller" + } + } + }, + "people": { + "action_label": { + "detach_image": "Bilder ablösen", + "merge_face": "Gesichter zusammenführen", + "move_face": "Gesicht verschieben" + }, + "confirm_image_detach": "Ablösen", + "title": "Bilder ablösen" + }, + "sharing": { + "add_share": "Freigabe hinzufügen", + "copy_link": "Link kopieren", + "delete": "Freigabe löschen", + "more": "mehr infos", + "no_shares_found": "Keine Freigaben gefunden", + "public_link": "Öffentlicher Link", + "title": "Freigabeoptionen" + } + }, + "sidemenu": { + "albums": "Alben", + "people": "Personen", + "photos": "Fotos", + "places": "Orte", + "settings": "Einstellungen" + }, + "timeline_filter": { + "date": { + "dropdown_all": "bis heute", + "dropdown_year": "alle bis {{year}}", + "label": "Datum" + } + }, + "title": { + "loading_album": "Lade Album", + "login": "Login", + "people": "Personen", + "settings": "Einstellungen" + } +} diff --git a/ui/src/extractedTranslations/de/translation_old.json b/ui/src/extractedTranslations/de/translation_old.json new file mode 100644 index 0000000..9b96d96 --- /dev/null +++ b/ui/src/extractedTranslations/de/translation_old.json @@ -0,0 +1,165 @@ +{ + "album_filter": { + "sort": null + }, + "albums_page": { + "title": "Alben" + }, + "general": { + "action": { + "delete": "Löschen", + "more": "Mehr" + }, + "loading": { + "media": "Lade Medien", + "timeline": "Lade Timeline" + } + }, + "header": { + "search": { + "result_type": { + "media": null, + "photos": "Fotos" + } + } + }, + "people_page": { + "action_label": { + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "unlabeled_person": null + }, + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + }, + "tableselect_face_group": { + "search_faces_placeholder": null + }, + "tableselect_image_faces": { + "search_images_placeholder": null + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Administrator", + "capabilities": null + } + } + } + }, + "share_page": { + "protected_share": { + "password_required_error": null + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "Album Optionen", + "title_placeholder": null + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "" + } + }, + "media": { + "album": "" + }, + "sharing": { + "delete": null, + "more": null, + "table_header": "Öffentliche Freigabe" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + } +} diff --git a/ui/src/extractedTranslations/en/translation.json b/ui/src/extractedTranslations/en/translation.json new file mode 100644 index 0000000..d590dcb --- /dev/null +++ b/ui/src/extractedTranslations/en/translation.json @@ -0,0 +1,371 @@ +{ + "album_filter": { + "only_favorites": "Show only favorites", + "sort": "Sort", + "sort_by": "Sort by", + "sort_direction": "Sorting direction", + "order_direction": { + "ascending": "Ascending", + "descending": "Descending" + }, + "sorting_options": { + "date_imported": "Date imported", + "date_shot": "Date shot", + "title": "Title", + "type": "Kind" + } + }, + "general": { + "action": { + "add": "Add", + "cancel": "Cancel", + "remove": "Remove", + "save": "Save" + }, + "loading": { + "album": "Loading album", + "default": "Loading...", + "page": "Loading page", + "paginate": { + "faces": "Loading more people", + "media": "Loading more media" + }, + "shares": "Loading shares..." + } + }, + "header": { + "search": { + "loading": "Loading results...", + "no_results": "No results found", + "placeholder": "Search", + "result_type": { + "albums": "Albums", + "media": "Media" + } + } + }, + "login_page": { + "field": { + "password": "Password", + "submit": "Sign in", + "username": "Username" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Photo path", + "placeholder": "/path/to/photos" + }, + "submit": "Setup Photoview" + }, + "title": "Initial Setup" + }, + "welcome": "Welcome to Photoview" + }, + "meta": { + "description": "Simple and User-friendly Photo Gallery for Personal Servers" + }, + "people_page": { + "action_label": { + "change_label": "Change label", + "detach_images": "Detach face", + "merge_face": "Merge face", + "merge_people": "Merge face", + "move_faces": "Move faces" + }, + "face_group": { + "label_placeholder": "Label", + "unlabeled": "Unlabeled", + "unlabeled_person": "Unlabeled person" + }, + "modal": { + "action": { + "merge": "Merge", + "next": "Next" + }, + "detach_image_faces": { + "action": { + "detach": "Detach image faces", + "select_images": "Select images to detach" + }, + "description": "Detach selected images of this face group and move them to a new face groups", + "title": "Detach Image Faces" + }, + "merge_face_groups": { + "destination_description": "Select the face group that other groups should be merged into.", + "destination_table": { + "title": "Select the destination face" + }, + "sources_description": "Select all face groups that will be merged into the destination group.", + "sources_table": { + "title": "Select one or more source faces to merge into:" + }, + "title": "Merge Face Groups" + }, + "move_image_faces": { + "description": "Move selected images of this face group to another face group", + "destination_face_group_table": { + "move_action": "Move image faces", + "title": "Select destination face group" + }, + "image_select_table": { + "next_action": "Next", + "title": "Select images to move" + }, + "title": "Move Image Faces" + } + }, + "recognize_unlabeled_faces_button": "Recognize unlabeled faces", + "tableselect_face_group": { + "search_faces_placeholder": "Search faces..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Search images..." + } + }, + "photos_page": { + "title": "Timeline" + }, + "places_page": { + "title": "Places" + }, + "routes": { + "page_not_found": "Page not found" + }, + "settings": { + "concurrent_workers": { + "description": "The maximum amount of scanner jobs that is allowed to run at once", + "title": "Scanner concurrent workers" + }, + "logout": "Log out", + "periodic_scanner": { + "checkbox_label": "Enable periodic scanner", + "field": { + "description": "How often the scanner should perform automatic scans of all users", + "label": "Periodic scan interval" + }, + "interval_unit": { + "days": "Days", + "hour": "Hour", + "minutes": "Minutes", + "months": "Months", + "seconds": "Seconds" + }, + "title": "Periodic scanner" + }, + "scanner": { + "description": "Will scan all users for new or updated media", + "scan_all_users": "Scan all users", + "title": "Scanner" + }, + "user_preferences": { + "change_language": { + "description": "Change website language specific for this user", + "label": "Website language" + }, + "language_selector": { + "placeholder": "Select language" + }, + "theme": { + "auto": { + "label": "Same as system" + }, + "dark": { + "label": "Dark" + }, + "description": "Change the appearance of the website", + "light": { + "label": "Light" + }, + "title": "Theme preferences" + }, + "title": "User preferences" + }, + "users": { + "add_user": { + "submit": "Add user" + }, + "confirm_delete_user": { + "action": "Delete {{user}}", + "description": "<0>Are you sure, you want to delete <1>?

    This action cannot be undone

    ", + "title": "Delete user" + }, + "password_reset": { + "description": "Change password for <1>{{username}}", + "form": { + "label": "New password", + "placeholder": "password", + "submit": "Change password" + }, + "title": "Change password" + }, + "table": { + "column_names": { + "action": "Action", + "capabilities": "Capabilities", + "photo_path": "Photo path", + "username": "Username" + }, + "new_user": "New user", + "row": { + "action": { + "change_password": "Change password", + "delete": "Delete", + "edit": "Edit", + "scan": "Scan" + } + } + }, + "title": "Users" + }, + "version_info": { + "build_date_title": "Build date", + "title": "Photoview Version", + "version_title": "Release Version" + } + }, + "share_page": { + "media": { + "title": "Shared media" + }, + "protected_share": { + "description": "This share is protected with a password.", + "password_required_error": "Password is required", + "title": "Protected share" + }, + "share_not_found": "Share not found", + "share_not_found_description": "Maybe the share has expired or has been deleted.", + "wrong_password": "Wrong password, please try again." + }, + "sidebar": { + "album": { + "album_cover": "Album cover", + "download": { + "high-resolutions": { + "description": "High resolution jpegs of RAW images", + "title": "High resolutions" + }, + "originals": { + "description": "The original images and videos", + "title": "Originals" + }, + "thumbnails": { + "description": "Low resolution images, no videos", + "title": "Thumbnails" + }, + "web-videos": { + "description": "Videos that have been optimized for web", + "title": "Converted videos" + } + }, + "reset_cover": "Reset cover photo", + "set_cover": "Set as album cover photo", + "title_placeholder": "Album title" + }, + "download": { + "filesize": { + "byte_one": "{{count}} Byte", + "byte_other": "{{count}} Bytes", + "giga_byte_one": "{{count}} GB", + "giga_byte_other": "{{count}} GB", + "kilo_byte_one": "{{count}} KB", + "kilo_byte_other": "{{count}} KB", + "mega_byte_one": "{{count}} MB", + "mega_byte_other": "{{count}} MB", + "tera_byte_one": "{{count}} TB", + "tera_byte_other": "{{count}} TB" + }, + "table_columns": { + "dimensions": "Dimensions", + "file_size": "Size", + "file_type": "Type", + "name": "Name" + }, + "title": "Download" + }, + "location": { + "title": "Location" + }, + "media": { + "album_path": "Album path", + "exif": { + "description": "Description", + "exposure_program": { + "action_program": "Action program", + "aperture_priority": "Aperture priority", + "bulb": "Bulb", + "creative_program": "Creative program", + "landscape_mode": "Landscape mode", + "manual": "Manual", + "normal_program": "Normal program", + "not_defined": "Not defined", + "portrait_mode": "Portrait mode", + "shutter_priority": "Shutter priority" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Did not fire", + "fired": "Fired", + "no_flash": "No Flash", + "no_flash_function": "No flash function", + "off": "Off", + "on": "On", + "red_eye_reduction": "Red-eye reduction", + "return_detected": "Return detected", + "return_not_detected": "Return not detected" + }, + "name": { + "aperture": "Aperture", + "camera": "Camera", + "coordinates": "Coordinates", + "date_shot": "Date shot", + "exposure": "Exposure", + "exposure_program": "Program", + "flash": "Flash", + "focal_length": "Focal length", + "iso": "ISO", + "lens": "Lens", + "maker": "Maker" + } + } + }, + "people": { + "action_label": { + "detach_image": "Detach image", + "merge_face": "Merge face", + "move_face": "Move face" + }, + "confirm_image_detach": "Are you sure you want to detach this image?", + "title": "People" + }, + "sharing": { + "add_share": "Add shares", + "copy_link": "Copy Link", + "delete": "Delete", + "more": "More", + "no_shares_found": "No shares found", + "public_link": "Public Link", + "title": "Sharing options" + } + }, + "sidemenu": { + "albums": "Albums", + "people": "People", + "photos": "Timeline", + "places": "Places", + "settings": "Settings" + }, + "timeline_filter": { + "date": { + "dropdown_all": "From today", + "dropdown_year": "{{year}} and earlier", + "label": "Date" + } + }, + "title": { + "loading_album": "Loading album", + "login": "Login", + "people": "People", + "settings": "Settings" + } +} diff --git a/ui/src/extractedTranslations/en/translation_old.json b/ui/src/extractedTranslations/en/translation_old.json new file mode 100644 index 0000000..b1d3752 --- /dev/null +++ b/ui/src/extractedTranslations/en/translation_old.json @@ -0,0 +1,103 @@ +{ + "albums_page": { + "title": "Albums" + }, + "general": { + "action": { + "delete": "Delete", + "more": "More" + }, + "loading": { + "media": "Loading media", + "timeline": "Loading timeline" + } + }, + "header": { + "search": { + "result_type": { + "photos": "Photos" + } + } + }, + "people_page": { + "face_group": { + "action": { + "add_label": "Add Label", + "change_label": "Change Label", + "detach_face": "Detach Face", + "merge_face": "Merge Face", + "move_faces": "Move Faces" + } + }, + "modal": { + "merge_face_groups": { + "description": "All images within this face group will be merged into the selected face group." + }, + "merge_people_groups": { + "description": "All images within this face group will be merged into the selected face group.", + "destination_table": { + "title": "Select the destination face" + }, + "title": "Merge Face Groups" + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": "Search faces..." + }, + "select_image_faces": { + "search_images_placeholder": "Search images..." + } + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "The filter to use when generating thumbnails", + "filter": { + "box": "Box", + "catmull_rom": "Catmull-Rom", + "Lanczos": "Lanczos (highest quality)", + "linear": "Linear", + "mitchell_netravali": "Mitchell-Netravali", + "nearest_neighbor": "Nearest Neighbor (default)" + }, + "label": "Downsampling method" + }, + "title": "Thumbnail preferences" + }, + "users": { + "table": { + "column_names": { + "admin": "Admin" + } + } + } + }, + "sidebar": { + "album": { + "cover_photo": "Album cover", + "title": "Album options" + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "{{count}} GB", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "{{count}} KB", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "{{count}} MB", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "{{count}} TB" + } + }, + "media": { + "album": "Album" + }, + "sharing": { + "table_header": "Public shares" + } + } +} diff --git a/ui/src/extractedTranslations/es/translation.json b/ui/src/extractedTranslations/es/translation.json new file mode 100644 index 0000000..4aa7aaa --- /dev/null +++ b/ui/src/extractedTranslations/es/translation.json @@ -0,0 +1,376 @@ +{ + "album_filter": { + "only_favorites": "Solo mostrar favoritos", + "sort": "Clasificación", + "sort_by": "Ordenar por", + "sort_direction": "Dirección de ordenación", + "order_direction": { + "ascending": "Ascendente", + "descending": "Descendente" + }, + "sorting_options": { + "date_imported": "Fecha de importado", + "date_shot": "Fecha de la foto", + "title": "Título", + "type": "Tipo" + } + }, + "general": { + "action": { + "add": "Añadir", + "cancel": "Cancelar", + "remove": "Borrar", + "save": "Guardar" + }, + "loading": { + "album": "Cargando álbum", + "default": "Cargando...", + "page": "Cargando página", + "paginate": { + "faces": "Cargando más gente", + "media": "Cargando más archivos" + }, + "shares": "Cargando compartidos..." + } + }, + "header": { + "search": { + "loading": "Cargando resultados...", + "no_results": "No se han encontrado resultados", + "placeholder": "Buscar", + "result_type": { + "albums": "Álbumes", + "media": "" + } + } + }, + "login_page": { + "field": { + "password": "Contraseña", + "submit": "Iniciar sesión", + "username": "Usuario" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Ruta de las fotos", + "placeholder": "/carpeta/de/fotos" + }, + "submit": "Configurar Photoview" + }, + "title": "Configuración inicial" + }, + "welcome": "Bienvenido a Photoview" + }, + "meta": { + "description": "Una galería de fotos sencilla y fácil de usar para servidores personales" + }, + "people_page": { + "action_label": { + "change_label": "", + "detach_images": "", + "merge_face": "", + "merge_people": "", + "move_faces": "" + }, + "face_group": { + "label_placeholder": "Etiqueta", + "unlabeled": "Sin etiquetar", + "unlabeled_person": "" + }, + "modal": { + "action": { + "merge": "", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "", + "select_images": "" + }, + "description": "", + "title": "" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "" + }, + "move_image_faces": { + "description": "", + "destination_face_group_table": { + "move_action": "", + "title": "" + }, + "image_select_table": { + "next_action": "", + "title": "" + }, + "title": "" + } + }, + "recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar", + "tableselect_face_group": { + "search_faces_placeholder": "" + }, + "tableselect_image_faces": { + "search_images_placeholder": "" + } + }, + "photos_page": { + "title": "Fotos" + }, + "places_page": { + "title": "Lugares" + }, + "routes": { + "page_not_found": "Página no encontrada" + }, + "settings": { + "concurrent_workers": { + "description": "La cantidad máxima de tareas de escaneo que pueden ejecutarse al mismo tiempo", + "title": "Tareas de escaneo concurrentes" + }, + "logout": "Cerrar sesión", + "periodic_scanner": { + "checkbox_label": "Activar el escaneo periódico", + "field": { + "description": "Con qué frecuencia debe ejecutarse un escaneo automático de todos los usuarios", + "label": "Intervalo del escaneo periódico" + }, + "interval_unit": { + "days": "Días", + "hour": "Horas", + "minutes": "Minutos", + "months": "Meses", + "seconds": "Segundos" + }, + "title": "Escaneo periódico" + }, + "scanner": { + "description": "Escaneará todos los usuarios para buscar archivos nuevos o actualizados", + "scan_all_users": "Escanear todos los usuarios", + "title": "Escáner" + }, + "user_preferences": { + "change_language": { + "description": "Cambia el idioma de la página para este usuario", + "label": "Idioma de la página" + }, + "language_selector": { + "placeholder": "Seleccionar idioma" + }, + "theme": { + "auto": { + "label": "" + }, + "dark": { + "label": "" + }, + "description": "", + "light": { + "label": "" + }, + "title": "" + }, + "title": "Preferencias de usuario" + }, + "users": { + "add_user": { + "submit": "Añadir usuario" + }, + "confirm_delete_user": { + "action": "Eliminar {{user}}", + "description": "<0>¿Estás seguro de que quieres eliminar a <1>?

    Esta acción no puede deshacerse

    ", + "title": "Eliminar usuario" + }, + "password_reset": { + "description": "Cambiar contraseña a <1>{{username}}", + "form": { + "label": "Nueva contraseña", + "placeholder": "contraseña", + "submit": "Cambiar contraseña" + }, + "title": "Cambiar contraseña" + }, + "table": { + "column_names": { + "action": "Acción", + "capabilities": "", + "photo_path": "Ruta de las fotos", + "username": "Usuario" + }, + "new_user": "Nuevo usuario", + "row": { + "action": { + "change_password": "Cambiar contraseña", + "delete": "Eliminar", + "edit": "Editar", + "scan": "Escanear" + } + } + }, + "title": "Usuarios" + }, + "version_info": { + "build_date_title": "", + "title": "", + "version_title": "" + } + }, + "share_page": { + "media": { + "title": "Archivos compartidos" + }, + "protected_share": { + "description": "Esta compartición está protegida por contraseña.", + "password_required_error": "", + "title": "Compartición protegida" + }, + "share_not_found": "Compartición no encontrada", + "share_not_found_description": "Quizá la compartición ha expirado o ha sido eliminada.", + "wrong_password": "Contraseña incorrecta, por favor inténtelo de nuevo." + }, + "sidebar": { + "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", + "title_placeholder": "" + }, + "download": { + "filesize": { + "byte_one": "", + "byte_many": "", + "byte_other": "", + "giga_byte_one": "", + "giga_byte_many": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_many": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_many": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_many": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Dimensiones", + "file_size": "Tamaño", + "file_type": "Tipo", + "name": "Nombre" + }, + "title": "Descargar" + }, + "location": { + "title": "" + }, + "media": { + "album_path": "", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Programa de acción", + "aperture_priority": "Prioridad de abertura", + "bulb": "Flash", + "creative_program": "Programa creativo", + "landscape_mode": "Modo panorámico", + "manual": "Manual", + "normal_program": "Programa normal", + "not_defined": "No definido", + "portrait_mode": "Modo retrato", + "shutter_priority": "Prioridad de obturador" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "No se disparó", + "fired": "Disparado", + "no_flash": "Sin Flash", + "no_flash_function": "Sin función de flash", + "off": "Apagado", + "on": "Encendido", + "red_eye_reduction": "Reducción de ojos rojos", + "return_detected": "Retorno detectado", + "return_not_detected": "Retorno no detectado" + }, + "name": { + "aperture": "Abertura", + "camera": "Cámara", + "coordinates": "", + "date_shot": "Fecha de captura", + "exposure": "Exposición", + "exposure_program": "Programa", + "flash": "Flash", + "focal_length": "Distancia focal", + "iso": "ISO", + "lens": "Lente", + "maker": "Fabricante" + } + } + }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "confirm_image_detach": "", + "title": "" + }, + "sharing": { + "add_share": "Añadir compartido", + "copy_link": "Copiar enlace", + "delete": "", + "more": "", + "no_shares_found": "No se encontraron compartidos", + "public_link": "Enlace público", + "title": "Opciones de compartir" + } + }, + "sidemenu": { + "albums": "Álbumes", + "people": "Personas", + "photos": "Fotos", + "places": "Lugares", + "settings": "Opciones" + }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, + "title": { + "loading_album": "Cargando álbum", + "login": "", + "people": "Personas", + "settings": "Opciones" + } +} diff --git a/ui/src/extractedTranslations/es/translation_old.json b/ui/src/extractedTranslations/es/translation_old.json new file mode 100644 index 0000000..43d2086 --- /dev/null +++ b/ui/src/extractedTranslations/es/translation_old.json @@ -0,0 +1,173 @@ +{ + "album_filter": { + "sort": null + }, + "albums_page": { + "title": "Álbumes" + }, + "general": { + "action": { + "delete": "Eliminar", + "more": null + }, + "loading": { + "media": "Cargando archivos", + "timeline": "Cargando línea temporal" + } + }, + "header": { + "search": { + "result_type": { + "media": null, + "photos": "Fotos" + } + } + }, + "people_page": { + "action_label": { + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "unlabeled_person": null + }, + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": "", + "destination_table": { + "title": null + }, + "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + }, + "tableselect_face_group": { + "search_faces_placeholder": null + }, + "tableselect_image_faces": { + "search_images_placeholder": null + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Administrador", + "capabilities": null + } + } + }, + "version_info": { + "build_date_title": null, + "title": null, + "version_title": null + } + }, + "share_page": { + "protected_share": { + "password_required_error": null + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "opciones de álbum", + "title_placeholder": null + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "" + } + }, + "media": { + "album": "" + }, + "sharing": { + "delete": null, + "more": null, + "table_header": "Compartidos públicos" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + }, + "title": { + "login": null + } +} diff --git a/ui/src/extractedTranslations/eu/translation.json b/ui/src/extractedTranslations/eu/translation.json new file mode 100644 index 0000000..1368b61 --- /dev/null +++ b/ui/src/extractedTranslations/eu/translation.json @@ -0,0 +1,364 @@ +{ + "album_filter": { + "only_favorites": "Erakutsi gogokoak bakarrik", + "sort": "Sailkatu", + "sort_by": "Sailkatu honen arabera", + "sort_direction": "Sailkatze norabidea", + "order_direction": { + "ascending": "Goranzkoa", + "descending": "Beheranzkoa" + }, + "sorting_options": { + "date_imported": "Gehitutako data", + "date_shot": "Egindako data", + "title": "Izena", + "type": "Mota" + } + }, + "general": { + "action": { + "add": "Gehitu", + "cancel": "Utzi", + "remove": "Kendu", + "save": "Gorde" + }, + "loading": { + "album": "Albuma kargatzen", + "default": "Kargatzen…", + "page": "Orrialdea kargatzen", + "paginate": { + "faces": "Jende gehiago kargatzen", + "media": "Eduki gehiago kargatzen" + }, + "shares": "Partekatutakoak kargatzen…" + } + }, + "header": { + "search": { + "loading": "Emaitzak kargatzen…", + "no_results": "Ez da emaitzarik aurkitu", + "placeholder": "Bilatu", + "result_type": { + "albums": "Albumak", + "media": "Edukia" + } + } + }, + "login_page": { + "field": { + "password": "Pasahitza", + "submit": "Hasi saioa", + "username": "Erabiltzailea" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Argazkien bidea", + "placeholder": "/argazkiak/dauden/direktorioa" + }, + "submit": "Konfiguratu Photoview" + }, + "title": "Aurreneko konfigurazioa" + }, + "welcome": "Ongi etorri Photoview-ra" + }, + "meta": { + "description": "Zerbitzari pertsonaletarako argazki-galeria samur eta erabilterreza" + }, + "people_page": { + "action_label": { + "change_label": "Aldatu etiketa", + "detach_images": "Kendu aurpegia", + "merge_people": "Bateratu aurpegiak", + "move_faces": "Mugitu aurpegiak" + }, + "face_group": { + "label_placeholder": "Etiketa", + "unlabeled": "Etiketarik gabea", + "unlabeled_person": "Etiketarik gabeko pertsona" + }, + "modal": { + "action": { + "merge": "Bateratu" + }, + "detach_image_faces": { + "action": { + "detach": "Kendu irudiko aurpegiak", + "select_images": "Aukeratu aurpegiak kentzeko irudiak" + }, + "description": "Kendu aukeratutako irudiak aurpegi-multzo honetatik eta mugitu beste aurpegi-multzo batera", + "title": "Kendu irudiko aurpegiak" + }, + "merge_face_groups": { + "description": "Aurpegi-multzo honetako irudi guztiak aukeratutako aurpegi-multzoarekin bateratuko dira.", + "destination_table": { + "title": "Aukeratu zein aurpegira mugituko d(ir)en" + }, + "title": "Bateratu aurpegi-multzoak" + }, + "move_image_faces": { + "description": "Mugitu aurpegi-multzo honetan aukeratutako irudiak beste aurpegi-multzo batera", + "destination_face_group_table": { + "move_action": "Mugitu irudiko aurpegiak", + "title": "Aukeratu zein aurpegi-multzora mugituko d(ir)en" + }, + "image_select_table": { + "next_action": "Hurrengoa", + "title": "Aukeratu mugitzeko irudiak" + }, + "title": "Mugitu irudiko aurpegiak" + } + }, + "recognize_unlabeled_faces_button": "Antzeman etiketarik gabeko aurpegiak", + "tableselect_face_group": { + "search_faces_placeholder": "Bilatu aurpegiak…" + }, + "tableselect_image_faces": { + "search_images_placeholder": "Bilatu irudiak…" + } + }, + "photos_page": { + "title": "Denbora-lerroa" + }, + "places_page": { + "title": "Tokiak" + }, + "routes": { + "page_not_found": "Ez da orrialdea aurkitu" + }, + "settings": { + "concurrent_workers": { + "description": "Aldi berean jardun dezaketen baimendutako azterketa-lan kopuru maximoa", + "title": "Aldibereko azterketa-lanak" + }, + "logout": "Amaitu saioa", + "periodic_scanner": { + "checkbox_label": "Aktibatu aldizkako azterketa", + "field": { + "description": "Zenbatero aztertu beharko lirateken automatikoki erabiltzaile guztiak", + "label": "Aldizkako azterketaren maiztasuna" + }, + "interval_unit": { + "days": "Egun", + "hour": "Ordu", + "minutes": "Minutu", + "months": "Hilabete", + "seconds": "Segundu" + }, + "title": "Aldizkako azterketa" + }, + "scanner": { + "description": "Erabiltzaile guztiak aztertuko ditugu eduki berri edo eguneratuen bila", + "scan_all_users": "Aztertu erabiltzaile guztiak", + "title": "Azterketa" + }, + "user_preferences": { + "change_language": { + "description": "Aldatu webgunearen hizkuntza erabiltzaile honentzat", + "label": "Webgunearen hizkuntza" + }, + "language_selector": { + "placeholder": "Aukeratu hizkuntza" + }, + "theme": { + "auto": { + "label": "Sistemak darabilena" + }, + "dark": { + "label": "Iluna" + }, + "description": "Aldatu webgunearen itxura", + "light": { + "label": "Argia" + }, + "title": "Itxura-aukerak" + }, + "title": "Erabiltzaile-ezarpenak" + }, + "users": { + "add_user": { + "submit": "Gehitu erabiltzailea" + }, + "confirm_delete_user": { + "action": "Ezabatu {{user}}", + "description": "<0>Ziur al zaude <1> ezabatu nahi duzula?

    Eragiketa hau ezin da desegin

    ", + "title": "Ezabatu erabiltzailea" + }, + "password_reset": { + "description": "Aldatu <1>{{username}}(r)en pasahitza", + "form": { + "label": "Pasahitz berria", + "placeholder": "pasahitza", + "submit": "Aldatu pasahitza" + }, + "title": "Aldatu pasahitza" + }, + "table": { + "column_names": { + "action": "Eragiketa", + "capabilities": "Ahalmenak", + "photo_path": "Argazkien bidea", + "username": "Erabiltzailea" + }, + "new_user": "Erabiltzaile berria", + "row": { + "action": { + "change_password": "Aldatu pasahitza", + "delete": "Ezabatu", + "edit": "Editatu", + "scan": "Aztertu" + } + } + }, + "title": "Erabiltzaileak" + }, + "version_info": { + "build_date_title": "Build-aren data", + "title": "Photoview-ren bertsioa", + "version_title": "Argitaratze-bertsioa" + } + }, + "share_page": { + "media": { + "title": "Partekatutako edukia" + }, + "protected_share": { + "description": "Partekatutakoa pasahitz bidez dago babestuta.", + "password_required_error": "Pasahitza beharrezkoa da", + "title": "Babestu partekatutakoa" + }, + "share_not_found": "Ez da partekatutakoa aurkitu", + "share_not_found_description": "Agian partekatutakoa iraungi da edo ezabatua izan da.", + "wrong_password": "Pasahitz okerra. Mesedez, saia zaitez berriro." + }, + "sidebar": { + "album": { + "album_cover": "Albumaren azala", + "download": { + "high-resolutions": { + "description": "RAW irudien bereizmen handiko jpeg-ak", + "title": "Bereizmen handikoak" + }, + "originals": { + "description": "Jatorrizko irudi eta bideoak", + "title": "Jatorrizkoak" + }, + "thumbnails": { + "description": "Bereizmen txikiko irudiak, ez bideoak", + "title": "Irudi txikiak" + }, + "web-videos": { + "description": "Sarerako optimizatu diren bideoak", + "title": "Bihurtutako bideoak" + } + }, + "reset_cover": "Berrezarri azalaren argazkia", + "set_cover": "Ezarri albumaren azal bezala", + "title_placeholder": "Albumaren titulua" + }, + "download": { + "filesize": { + "byte_one": "Byte {{count}}", + "byte_other": "{{count}} Byte", + "giga_byte_one": "GB {{count}}", + "giga_byte_other": "{{count}} GB", + "kilo_byte_one": "KB {{count}}", + "kilo_byte_other": "{{count}} KB", + "mega_byte_one": "MB {{count}}", + "mega_byte_other": "{{count}} MB", + "tera_byte_one": "TB {{count}}", + "tera_byte_other": "{{count}} TB" + }, + "table_columns": { + "dimensions": "Neurriak", + "file_size": "Tamaina", + "file_type": "Mota", + "name": "Izena" + }, + "title": "Deskargatu" + }, + "location": { + "title": "Kokapena" + }, + "media": { + "album_path": "Albumaren bidea", + "exif": { + "exposure_program": { + "action_program": "Kirola", + "aperture_priority": "Irekidurari lehentasuna", + "bulb": "Bulb", + "creative_program": "Sormenezkoa", + "landscape_mode": "Paisaia", + "manual": "Eskuzkoa", + "normal_program": "Arrunta", + "not_defined": "Ezarri gabe", + "portrait_mode": "Erretratua", + "shutter_priority": "Abiaduari lehentasuna" + }, + "flash": { + "auto": "Automatikoa", + "did_not_fire": "Ez da atera", + "fired": "Atera da", + "no_flash": "Flashik ez", + "no_flash_function": "Flash aukerarik ez", + "off": "Itzalita", + "on": "Piztuta", + "red_eye_reduction": "Begi gorrien gutxitzea", + "return_detected": "Kamerak antzeman du", + "return_not_detected": "Kamerak ez du antzeman" + }, + "name": { + "aperture": "Irekidura", + "camera": "Kamera", + "coordinates": "Koordenatuak", + "date_shot": "Egindako data", + "exposure": "Esposizioa", + "exposure_program": "Programa", + "flash": "Flasha", + "focal_length": "Foku-distantzia", + "iso": "Sentikortasuna", + "lens": "Objektiboa", + "maker": "Fabrikatzailea" + } + } + }, + "people": { + "action_label": { + "detach_image": "Kendu irudia", + "merge_face": "Bateratu aurpegia", + "move_face": "Mugitu aurpegia" + }, + "confirm_image_detach": "Ziur al zaude irudi hau kendu nahi duzula?", + "title": "Jendea" + }, + "sharing": { + "add_share": "Gehitu partekatzeak", + "copy_link": "Kopiatu esteka", + "delete": "Ezabatu", + "more": "Gehiago", + "no_shares_found": "Ez da partekatutakorik aurkitu", + "public_link": "Esteka publikoa", + "title": "Partekatze-aukerak" + } + }, + "sidemenu": { + "albums": "Albumak", + "people": "Jendea", + "photos": "Denbora-lerroa", + "places": "Tokiak", + "settings": "Ezarpenak" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Une honetatik aurrera", + "dropdown_year": "{{year}} eta zaharragoak", + "label": "Data" + } + }, + "title": { + "loading_album": "Albuma kargatzen", + "login": "Saioa", + "people": "Jendea", + "settings": "Ezarpenak" + } +} diff --git a/ui/src/extractedTranslations/fr/translation.json b/ui/src/extractedTranslations/fr/translation.json new file mode 100644 index 0000000..c653911 --- /dev/null +++ b/ui/src/extractedTranslations/fr/translation.json @@ -0,0 +1,376 @@ +{ + "album_filter": { + "only_favorites": "Afficher seulement les favoris", + "sort": "Trier", + "sort_by": "Trier par", + "sort_direction": "Sens du tri", + "order_direction": { + "ascending": "Croissant", + "descending": "Décroissant" + }, + "sorting_options": { + "date_imported": "Date d'importation", + "date_shot": "Date de prise de vue", + "title": "Titre", + "type": "Type" + } + }, + "general": { + "action": { + "add": "Ajouter", + "cancel": "Annuler", + "remove": "Supprimer", + "save": "Sauvegarder" + }, + "loading": { + "album": "Chargement de l'album", + "default": "Chargement...", + "page": "Chargement de la page", + "paginate": { + "faces": "Afficher plus de personnes", + "media": "Charger plus de médias" + }, + "shares": "Chargement des partages..." + } + }, + "header": { + "search": { + "loading": "Chargement des resultats...", + "no_results": "Aucun résultat trouvé", + "placeholder": "Rechercher", + "result_type": { + "albums": "Albums", + "media": "Médias" + } + } + }, + "login_page": { + "field": { + "password": "Mot de passe", + "submit": "Connexion", + "username": "Nom d'utilisateur" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Chemin des photos", + "placeholder": "/chemin/des/photos" + }, + "submit": "Configurer Photoview" + }, + "title": "Configuration initiale" + }, + "welcome": "Bienvenue sur Photoview" + }, + "meta": { + "description": "Galerie Photo simple et conviviale pour les Serveurs Personnels" + }, + "people_page": { + "action_label": { + "change_label": "Changer le label", + "detach_images": "Détacher le visage", + "merge_face": "Fusionner le visage", + "merge_people": "Fusionner les personnes", + "move_faces": "Déplacer les visages" + }, + "face_group": { + "label_placeholder": "Étiquette", + "unlabeled": "Sans étiquette", + "unlabeled_person": "Personne sans étiquette" + }, + "modal": { + "action": { + "merge": "Fusionner", + "next": "Suivant" + }, + "detach_image_faces": { + "action": { + "detach": "Détacher les visages des images", + "select_images": "Sélectionner les images de visages à détacher" + }, + "description": "Détachez les images sélectionnées de ce groupe de visages et déplacez-les vers un nouveau groupe de visages", + "title": "Détacher les visages des images" + }, + "merge_face_groups": { + "destination_description": "Sélectionnez le groupe de visages dans lequel les autres groupes seront fusionnés.", + "destination_table": { + "title": "Sélectionner le visage de destination" + }, + "sources_description": "Sélectionnez tous les groupes de visages qui seront fusionnés dans le groupe de destination.", + "sources_table": { + "title": "Sélectionnez un ou plusieurs groupes sources à fusionner :" + }, + "title": "Fusionner des groupes de visages" + }, + "move_image_faces": { + "description": "Déplacer les images sélectionnées de ce groupe de visages vers un autre groupe de visages", + "destination_face_group_table": { + "move_action": "Déplacer les visages de l'image", + "title": "Sélectionner le groupe de visages de destination" + }, + "image_select_table": { + "next_action": "Suivant", + "title": "Sélectionner les images à déplacer" + }, + "title": "Déplacer les visages des images" + } + }, + "recognize_unlabeled_faces_button": "Reconnaître les visages sans étiquette", + "tableselect_face_group": { + "search_faces_placeholder": "Rechercher des visages..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Rechercher des images..." + } + }, + "photos_page": { + "title": "Photos" + }, + "places_page": { + "title": "Lieux" + }, + "routes": { + "page_not_found": "Page non trouvée" + }, + "settings": { + "concurrent_workers": { + "description": "Nombre maximal de tâches d'analyse simultanées.", + "title": "Tâches d'analyse simultanées" + }, + "logout": "Se déconnecter", + "periodic_scanner": { + "checkbox_label": "Activer l'analyse périodique", + "field": { + "description": "Intervalle à laquelle l'analyse se déclenche pour tous les utilisateurs", + "label": "Intervalle d'analyse périodique" + }, + "interval_unit": { + "days": "Jours", + "hour": "Heures", + "minutes": "Minutes", + "months": "Mois", + "seconds": "Secondes" + }, + "title": "Analyse périodique" + }, + "scanner": { + "description": "Analyser tous les utilisateurs pour détecter de nouveaux médias ou des médias mis à jour", + "scan_all_users": "Analyser tous les utilisateurs", + "title": "Analyse" + }, + "user_preferences": { + "change_language": { + "description": "Changer la langue du site pour cet utilisateur", + "label": "Langue du site" + }, + "language_selector": { + "placeholder": "Choisir la langue" + }, + "theme": { + "auto": { + "label": "Par défaut (système)" + }, + "dark": { + "label": "Sombre" + }, + "description": "Changer l'apparence du site", + "light": { + "label": "Clair" + }, + "title": "Préférences de thème" + }, + "title": "Préférences Utilisateur" + }, + "users": { + "add_user": { + "submit": "Ajouter l'utilisateur" + }, + "confirm_delete_user": { + "action": "Supprimer {{user}}", + "description": "<0>Êtes-vous sûr de vouloir supprimer <1>?

    Cette action est irreversible

    ", + "title": "Supprimer l'utilisateur" + }, + "password_reset": { + "description": "Modifier le mot de passe de <1>{{username}}", + "form": { + "label": "Nouveau mot de passe", + "placeholder": "mot de passe", + "submit": "Changer le mot de passe" + }, + "title": "Changer le mot de passe" + }, + "table": { + "column_names": { + "action": "Action", + "capabilities": "Permissions", + "photo_path": "Emplacement des photos", + "username": "Nom d'utilisateur" + }, + "new_user": "Nouvel utilisateur", + "row": { + "action": { + "change_password": "Changer le mot de passe", + "delete": "Effacer", + "edit": "Éditer", + "scan": "Analyser" + } + } + }, + "title": "Utilisateurs" + }, + "version_info": { + "build_date_title": "Date de build", + "title": "Photoview Version", + "version_title": "Version actuelle" + } + }, + "share_page": { + "media": { + "title": "Média Partagé" + }, + "protected_share": { + "description": "Ce partage est protégé par un mot de passe.", + "password_required_error": "Le mot de passe est requis", + "title": "Partage protégé" + }, + "share_not_found": "Partage non trouvé", + "share_not_found_description": "Le partage est peut-être expiré ou supprimé.", + "wrong_password": "Mot de passe invalide, veuillez réessayer." + }, + "sidebar": { + "album": { + "album_cover": "Couverture d'Album", + "download": { + "high-resolutions": { + "description": "JPEGs Haute définition des images RAW", + "title": "Hautes résolutions" + }, + "originals": { + "description": "Images et vidéos originales", + "title": "Originaux" + }, + "thumbnails": { + "description": "Images Basse définitation, pas de vidéos", + "title": "Miniatures" + }, + "web-videos": { + "description": "Vidéos optimisées pour le Web", + "title": "Vidéos converties" + } + }, + "reset_cover": "Réinitialiser la photo de couverture", + "set_cover": "Définir une photo de couverture d'Album", + "title_placeholder": "Titre de l'Album" + }, + "download": { + "filesize": { + "byte_one": "{{count}} Octet", + "byte_many": "", + "byte_other": "{{count}} Octets", + "giga_byte_one": "{{count}} Go", + "giga_byte_many": "", + "giga_byte_other": "{{count}} Go", + "kilo_byte_one": "{{count}} Ko", + "kilo_byte_many": "", + "kilo_byte_other": "{{count}} Ko", + "mega_byte_one": "{{count}} Mo", + "mega_byte_many": "", + "mega_byte_other": "{{count}} Mo", + "tera_byte_one": "{{count}} To", + "tera_byte_many": "", + "tera_byte_other": "{{count}} To" + }, + "table_columns": { + "dimensions": "Dimensions", + "file_size": "Taille", + "file_type": "Type", + "name": "Nom" + }, + "title": "Télécharger" + }, + "location": { + "title": "Emplacement" + }, + "media": { + "album_path": "Emplacement de l'album", + "exif": { + "description": "Description", + "exposure_program": { + "action_program": "Programme d'action", + "aperture_priority": "Priorité d'ouverture", + "bulb": "Bulb", + "creative_program": "Programme créatif", + "landscape_mode": "Mode paysage", + "manual": "Manuel", + "normal_program": "Auto", + "not_defined": "Non défini", + "portrait_mode": "Mode portrait", + "shutter_priority": "Priorité d'obturateur" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Ne s'est pas déclenché", + "fired": "Déclenché", + "no_flash": "Pas de flash", + "no_flash_function": "Pas de fonction flash", + "off": "Désactivé", + "on": "Activé", + "red_eye_reduction": "Réduction yeux rouges", + "return_detected": "Retour détecté", + "return_not_detected": "Retour non détecté" + }, + "name": { + "aperture": "Ouverture", + "camera": "Modèle", + "coordinates": "Coordonnées", + "date_shot": "Date de prise de vue", + "exposure": "Exposition", + "exposure_program": "Programme", + "flash": "Flash", + "focal_length": "Focale", + "iso": "ISO", + "lens": "Objectif", + "maker": "Fabricant" + } + } + }, + "people": { + "action_label": { + "detach_image": "Détacher l'image", + "merge_face": "Fusionner le visage", + "move_face": "Déplacer le visage" + }, + "confirm_image_detach": "Êtes-vous sûr de vouloir détacher cette image ?", + "title": "Personnes" + }, + "sharing": { + "add_share": "Ajouter un partage", + "copy_link": "Copier le lien", + "delete": "Supprimer", + "more": "Plus", + "no_shares_found": "Aucun partage trouvé", + "public_link": "Lien public", + "title": "Options de partage" + } + }, + "sidemenu": { + "albums": "Albums", + "people": "Personnes", + "photos": "Chronologie", + "places": "Lieux", + "settings": "Paramètres" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Depuis aujourd'hui", + "dropdown_year": "{{year}} et avant", + "label": "Date" + } + }, + "title": { + "loading_album": "Chargement de l'album", + "login": "Connexion", + "people": "Personnes", + "settings": "Paramètres" + } +} diff --git a/ui/src/extractedTranslations/fr/translation_old.json b/ui/src/extractedTranslations/fr/translation_old.json new file mode 100644 index 0000000..2e24045 --- /dev/null +++ b/ui/src/extractedTranslations/fr/translation_old.json @@ -0,0 +1,148 @@ +{ + "albums_page": { + "title": "Albums" + }, + "general": { + "action": { + "delete": "Effacer", + "more": "Plus" + }, + "loading": { + "media": "Chargement du média", + "timeline": "Chargement de la timeline" + } + }, + "header": { + "search": { + "result_type": { + "photos": "Photos" + } + } + }, + "people_page": { + "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "unlabeled_person": null + }, + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "merge_people_groups": { + "description": "Toutes les images de ce groupe de visages seront fusionnées dans le groupe de visages sélectionné.", + "destination_table": { + "title": "Sélectionner le visage de destination" + }, + "title": "Fusionner des groupes de visages" + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Admin" + } + } + }, + "version_info": { + "build_date_title": null, + "title": null, + "version_title": null + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "Paramètres de l'album" + }, + "download": { + "filesize": { + "byte": "{{count}} Octets", + "byte_plural": "{{count}} Octets", + "giga_byte": "{{count}} Go", + "giga_byte_plural": "{{count}} Go", + "kilo_byte": "{{count}} Ko", + "kilo_byte_plural": "{{count}} Ko", + "mega_byte": "{{count}} Mo", + "mega_byte_plural": "{{count}} Mo", + "tera_byte": "{{count}} To", + "tera_byte_plural": "{{count}} To" + } + }, + "media": { + "album": "" + }, + "sharing": { + "table_header": "Partages publics" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + }, + "title": { + "login": null + } +} diff --git a/ui/src/extractedTranslations/it/translation.json b/ui/src/extractedTranslations/it/translation.json new file mode 100644 index 0000000..9b310b7 --- /dev/null +++ b/ui/src/extractedTranslations/it/translation.json @@ -0,0 +1,376 @@ +{ + "album_filter": { + "only_favorites": "Mostra solo i preferiti", + "sort": "Ordina", + "sort_by": "Ordina per", + "sort_direction": "Direzione ordinamento", + "order_direction": { + "ascending": "Crescente", + "descending": "Decrescente" + }, + "sorting_options": { + "date_imported": "Data importazione", + "date_shot": "Data scatto", + "title": "Titolo", + "type": "Tipo" + } + }, + "general": { + "action": { + "add": "Aggiungi", + "cancel": "Annulla", + "remove": "Rimuovi", + "save": "Salva" + }, + "loading": { + "album": "Caricamento album", + "default": "Caricamento...", + "page": "Caricamento pagina", + "paginate": { + "faces": "Carica più persone", + "media": "Carica più media" + }, + "shares": "Caricamento condivisi..." + } + }, + "header": { + "search": { + "loading": "Caricamento risultati...", + "no_results": "Nessun risultato trovato", + "placeholder": "Cerca", + "result_type": { + "albums": "Album", + "media": "" + } + } + }, + "login_page": { + "field": { + "password": "Password", + "submit": "Accedi", + "username": "Username" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Percorso foto", + "placeholder": "/path/to/photos" + }, + "submit": "Setup Photoview" + }, + "title": "Setup iniziale" + }, + "welcome": "Benvenuti su Photoview" + }, + "meta": { + "description": "Galleria fotografica semplice e user-friendly per il tuo server" + }, + "people_page": { + "action_label": { + "change_label": "Cambia etichetta", + "detach_images": "Separa viso", + "merge_face": "", + "merge_people": "Unisci viso", + "move_faces": "Sposta visi" + }, + "face_group": { + "label_placeholder": "Etichetta", + "unlabeled": "Senza etichetta", + "unlabeled_person": "Persona senza etichetta" + }, + "modal": { + "action": { + "merge": "Unisci", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "Separa immagini", + "select_images": "Seleziona le immagini da separare" + }, + "description": "Separa le immagini selezionate di questo volto e muovile in nuovo volto", + "title": "Separa immagini volto" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "Seleziona il volto di destinazione" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "Unisci immagini volto" + }, + "move_image_faces": { + "description": "Sposta le immagini selezionate di questo volto in un altro volto", + "destination_face_group_table": { + "move_action": "Muovi immagini", + "title": "Seleziona il volto di destinazione" + }, + "image_select_table": { + "next_action": "Avanti", + "title": "Seleziona immagini" + }, + "title": "Muovi immagini volto" + } + }, + "recognize_unlabeled_faces_button": "Identifica facce senza etichetta", + "tableselect_face_group": { + "search_faces_placeholder": "Ricerca visi..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Ricerca immagini..." + } + }, + "photos_page": { + "title": "Foto" + }, + "places_page": { + "title": "Luoghi" + }, + "routes": { + "page_not_found": "Pagina non trovata" + }, + "settings": { + "concurrent_workers": { + "description": "Numero massimo di scansioni contemporanee", + "title": "Scansioni contemporanee" + }, + "logout": "Log out", + "periodic_scanner": { + "checkbox_label": "Abilita scansioni periodiche", + "field": { + "description": "Frequenza delle scansioni automatiche per tutti gli utenti", + "label": "Intervallo delle scansioni" + }, + "interval_unit": { + "days": "Giorni", + "hour": "Ore", + "minutes": "Minuti", + "months": "Mesi", + "seconds": "Secondi" + }, + "title": "Scansioni periodiche" + }, + "scanner": { + "description": "Verranno effettuate delle scansioni per tutti gli utenti alla ricerca di media nuovi o aggiornati", + "scan_all_users": "Scansiona per tutti", + "title": "Scansioni" + }, + "user_preferences": { + "change_language": { + "description": "Cambia la lingua di Photoview per questo utente", + "label": "Lingua Photoview" + }, + "language_selector": { + "placeholder": "Seleziona lingua" + }, + "theme": { + "auto": { + "label": "Predefinito di sistema" + }, + "dark": { + "label": "Scuro" + }, + "description": "Cambia aspetto del sito", + "light": { + "label": "Chiaro" + }, + "title": "Preferenze tema" + }, + "title": "Preferenze utente" + }, + "users": { + "add_user": { + "submit": "Aggiungi utente" + }, + "confirm_delete_user": { + "action": "Elimina {{user}}", + "description": "<0>Sei sicuro di voler eliminare <1>?

    Non potrai tornare indietro

    ", + "title": "Cancella utente" + }, + "password_reset": { + "description": "Cambia password per <1>{{username}}", + "form": { + "label": "Nuova password", + "placeholder": "password", + "submit": "Cambia password" + }, + "title": "Cambia password" + }, + "table": { + "column_names": { + "action": "Azioni", + "capabilities": "Azioni consentite", + "photo_path": "Percorso foto", + "username": "Username" + }, + "new_user": "Nuovo utente", + "row": { + "action": { + "change_password": "Cambia password", + "delete": "Elimina", + "edit": "Modifica", + "scan": "Scansiona" + } + } + }, + "title": "Utenti" + }, + "version_info": { + "build_date_title": "Data build", + "title": "Versione Photoview", + "version_title": "Versione Release" + } + }, + "share_page": { + "media": { + "title": "Media condivisi" + }, + "protected_share": { + "description": "Questa condivisione è protetta da una password.", + "password_required_error": "La password è obbligatoria", + "title": "Condivisione protetta" + }, + "share_not_found": "Condivisone non trovata", + "share_not_found_description": "Forse la condivisione è scaduta o è stata eliminata.", + "wrong_password": "Password sbagliata, prova di nuovo." + }, + "sidebar": { + "album": { + "album_cover": "Copertina album", + "download": { + "high-resolutions": { + "description": "Jpeg ad alta risoluzione di immagini RAW", + "title": "Alta risoluzione" + }, + "originals": { + "description": "Immagini e video originali", + "title": "Originali" + }, + "thumbnails": { + "description": "Immagini a bassa risoluzione, video esclusi", + "title": "Miniatura" + }, + "web-videos": { + "description": "Video ottimizzati per la rete", + "title": "Video processati" + } + }, + "reset_cover": "Reimposta la foto di copertina", + "set_cover": "Imposta come foto di copertina", + "title_placeholder": "Titolo album" + }, + "download": { + "filesize": { + "byte_one": "", + "byte_many": "", + "byte_other": "", + "giga_byte_one": "", + "giga_byte_many": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_many": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_many": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_many": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Dimensioni", + "file_size": "Peso", + "file_type": "Formato", + "name": "Nome" + }, + "title": "Download" + }, + "location": { + "title": "Posizione" + }, + "media": { + "album_path": "Percorso album", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Programma sport", + "aperture_priority": "Priorità apertura", + "bulb": "Bulb", + "creative_program": "Programma creativo", + "landscape_mode": "Modalità orizzontale", + "manual": "Manuale", + "normal_program": "Programma normale", + "not_defined": "Non definito", + "portrait_mode": "Modalità verticale", + "shutter_priority": "Priorità otturatore" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Non attivato", + "fired": "Attivato", + "no_flash": "No Flash", + "no_flash_function": "Nessuna funzione di flash", + "off": "Off", + "on": "On", + "red_eye_reduction": "Riduzione occhi rossi", + "return_detected": "Rilevato", + "return_not_detected": "Non rilevato" + }, + "name": { + "aperture": "Apertura", + "camera": "Fotocamera", + "coordinates": "Coordinate", + "date_shot": "Data di scatto", + "exposure": "Esposizione", + "exposure_program": "Programma", + "flash": "Flash", + "focal_length": "Lunghezza focale", + "iso": "ISO", + "lens": "Lente", + "maker": "Produttore" + } + } + }, + "people": { + "action_label": { + "detach_image": "Scollega immagine", + "merge_face": "Unisci viso", + "move_face": "Sposta viso" + }, + "confirm_image_detach": "Sei sicuro di volere scollegare questa immagine?", + "title": "Persone" + }, + "sharing": { + "add_share": "Aggiungi condivisione", + "copy_link": "Copia il link", + "delete": "Elimina", + "more": "Altro", + "no_shares_found": "Nessuna condivisione trovata", + "public_link": "Link pubblico", + "title": "Opzioni di condivisione" + } + }, + "sidemenu": { + "albums": "Album", + "people": "Persone", + "photos": "Foto", + "places": "Luoghi", + "settings": "Impostazioni" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Da oggi", + "dropdown_year": "{{year}} e antecedenti", + "label": "Data" + } + }, + "title": { + "loading_album": "Caricamento album", + "login": "Login", + "people": "Persone", + "settings": "Impostazioni" + } +} diff --git a/ui/src/extractedTranslations/it/translation_old.json b/ui/src/extractedTranslations/it/translation_old.json new file mode 100644 index 0000000..99219a8 --- /dev/null +++ b/ui/src/extractedTranslations/it/translation_old.json @@ -0,0 +1,137 @@ +{ + "album_filter": { + "sort": null + }, + "albums_page": { + "title": "Album" + }, + "general": { + "action": { + "delete": "Elimina", + "more": "Altro" + }, + "loading": { + "media": "Caricamento media", + "timeline": "Caricamento timeline" + } + }, + "header": { + "search": { + "result_type": { + "media": null, + "photos": "Foto" + } + } + }, + "people_page": { + "action_label": { + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "face_group": { + "action": { + "add_label": "Aggiungi etichetta", + "change_label": "Cambia etichetta", + "detach_face": "Separa volto", + "merge_face": "Unisci volto", + "move_faces": "Sposta volti" + } + }, + "modal": { + "merge_face_groups": { + "description": null + }, + "merge_people_groups": { + "description": "Tutte le immagini di questo volto saranno unite alle immagini del volto selezionato", + "destination_table": { + "title": "Seleziona il volto di destinazione" + }, + "title": "Unisci immagini volto" + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": "Cerca volti..." + }, + "select_image_faces": { + "search_images_placeholder": "Cerca immagini..." + } + }, + "tableselect_face_group": { + "search_faces_placeholder": null + }, + "tableselect_image_faces": { + "search_images_placeholder": null + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Admin", + "capabilities": null + } + } + } + }, + "share_page": { + "protected_share": { + "password_required_error": null + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "Opzioni Album", + "title_placeholder": null + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "" + } + }, + "media": { + "album": "" + }, + "sharing": { + "delete": null, + "more": null, + "table_header": "Condivisioni pubbliche" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + } +} diff --git a/ui/src/extractedTranslations/ja/translation.json b/ui/src/extractedTranslations/ja/translation.json new file mode 100644 index 0000000..c59bf8f --- /dev/null +++ b/ui/src/extractedTranslations/ja/translation.json @@ -0,0 +1,371 @@ +{ + "album_filter": { + "only_favorites": "お気に入りのみ表示", + "sort": "並び替え", + "sort_by": "並び替え順", + "sort_direction": "並び替え方向", + "order_direction": { + "ascending": "昇順", + "descending": "降順" + }, + "sorting_options": { + "date_imported": "インポート日", + "date_shot": "撮影日", + "title": "タイトル", + "type": "種類" + } + }, + "general": { + "action": { + "add": "追加", + "cancel": "キャンセル", + "remove": "削除", + "save": "保存" + }, + "loading": { + "album": "アルバムを読み込み中", + "default": "読み込み中…", + "page": "ページを読み込み中", + "paginate": { + "faces": "人物を読み込み中", + "media": "メディアを読み込み中" + }, + "shares": "共有を読み込み中…" + } + }, + "header": { + "search": { + "loading": "検索結果を読み込み中…", + "no_results": "該当する結果がありません", + "placeholder": "検索", + "result_type": { + "albums": "アルバム", + "media": "メディア" + } + } + }, + "login_page": { + "field": { + "password": "パスワード", + "submit": "ログイン", + "username": "ユーザー名" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "写真の場所", + "placeholder": "/path/to/photos" + }, + "submit": "Photoview を初期設定" + }, + "title": "初期設定" + }, + "welcome": "Photoview へようこそ" + }, + "meta": { + "description": "個人サーバー向けのシンプルで使いやすい写真ギャラリー" + }, + "people_page": { + "action_label": { + "change_label": "ラベルを変更", + "detach_images": "顔グループを分割", + "merge_face": "顔グループを統合", + "merge_people": "顔グループを統合", + "move_faces": "顔グループを移動" + }, + "face_group": { + "label_placeholder": "ラベル", + "unlabeled": "ラベルなし", + "unlabeled_person": "ラベルなしの人物" + }, + "modal": { + "action": { + "merge": "統合", + "next": "次へ" + }, + "detach_image_faces": { + "action": { + "detach": "顔グループを分割", + "select_images": "写真を選択" + }, + "description": "選択した写真をこの顔グループから削除し、新しい顔グループに移動します", + "title": "写真の顔グループを分割" + }, + "merge_face_groups": { + "destination_description": "統合先の顔グループを選択してください。", + "destination_table": { + "title": "統合先の顔グループを選択" + }, + "sources_description": "統合する対象の顔グループをすべて選択してください。", + "sources_table": { + "title": "統合する対象の顔グループを1つ以上選択:" + }, + "title": "顔グループを統合" + }, + "move_image_faces": { + "description": "この顔グループの選択した写真を別の顔グループに移動します", + "destination_face_group_table": { + "move_action": "写真の顔グループを移動", + "title": "対象の顔グループを選択" + }, + "image_select_table": { + "next_action": "次へ", + "title": "移動する写真を選択" + }, + "title": "写真の顔グループを移動" + } + }, + "recognize_unlabeled_faces_button": "未ラベルの顔を認識", + "tableselect_face_group": { + "search_faces_placeholder": "顔グループを検索…" + }, + "tableselect_image_faces": { + "search_images_placeholder": "写真を検索…" + } + }, + "photos_page": { + "title": "タイムライン" + }, + "places_page": { + "title": "場所" + }, + "routes": { + "page_not_found": "ページが見つかりません" + }, + "settings": { + "concurrent_workers": { + "description": "同時に実行できる自動スキャンの最大数", + "title": "スキャン同時実行数" + }, + "logout": "ログアウト", + "periodic_scanner": { + "checkbox_label": "定期スキャンを有効にする", + "field": { + "description": "すべてのユーザーに対して自動スキャンを実行する頻度", + "label": "定期スキャン間隔" + }, + "interval_unit": { + "days": "日", + "hour": "時間", + "minutes": "分", + "months": "か月", + "seconds": "秒" + }, + "title": "定期スキャン" + }, + "scanner": { + "description": "すべてのユーザーに対して新しく追加または更新されたメディアをスキャンします", + "scan_all_users": "すべてのユーザーをスキャン", + "title": "スキャン" + }, + "user_preferences": { + "change_language": { + "description": "このユーザーの言語を変更", + "label": "言語" + }, + "language_selector": { + "placeholder": "言語を選択" + }, + "theme": { + "auto": { + "label": "システム設定に従う" + }, + "dark": { + "label": "ダーク" + }, + "description": "サイトの外観を変更", + "light": { + "label": "ライト" + }, + "title": "テーマの設定" + }, + "title": "ユーザー設定" + }, + "users": { + "add_user": { + "submit": "ユーザーを追加" + }, + "confirm_delete_user": { + "action": "{{user}} を削除", + "description": "<0><1> を削除してもよろしいですか?

    この操作は元に戻せません

    ", + "title": "ユーザーを削除" + }, + "password_reset": { + "description": "<1>{{username}} のパスワードを変更", + "form": { + "label": "新しいパスワード", + "placeholder": "パスワード", + "submit": "パスワードを変更" + }, + "title": "パスワードを変更" + }, + "table": { + "column_names": { + "action": "操作", + "capabilities": "権限", + "photo_path": "写真の場所", + "username": "ユーザー名" + }, + "new_user": "新しいユーザー", + "row": { + "action": { + "change_password": "パスワードを変更", + "delete": "削除", + "edit": "編集", + "scan": "スキャン" + } + } + }, + "title": "ユーザー" + }, + "version_info": { + "build_date_title": "ビルド日", + "title": "Photoview バージョン", + "version_title": "リリースバージョン" + } + }, + "share_page": { + "media": { + "title": "共有メディア" + }, + "protected_share": { + "description": "この共有はパスワードで保護されています。", + "password_required_error": "パスワードを入力してください", + "title": "保護された共有" + }, + "share_not_found": "この共有が見つかりません", + "share_not_found_description": "この共有は期限切れまたは削除された可能性があります。", + "wrong_password": "パスワードが正しくありません。パスワードを確認して再試行してください。" + }, + "sidebar": { + "album": { + "album_cover": "カバー", + "download": { + "high-resolutions": { + "description": "RAW ファイルから生成した高解像度 JPEG 画像", + "title": "高解像度" + }, + "originals": { + "description": "元の写真と動画", + "title": "オリジナルファイル" + }, + "thumbnails": { + "description": "低解像度写真 (動画は含まれません)", + "title": "サムネイル" + }, + "web-videos": { + "description": "Web表示用に最適化された動画", + "title": "変換済み動画" + } + }, + "reset_cover": "カバー画像をリセット", + "set_cover": "カバー画像として設定", + "title_placeholder": "アルバムタイトル" + }, + "download": { + "filesize": { + "byte_one": "{{count}} バイト", + "byte_other": "{{count}} バイト", + "giga_byte_one": "{{count}} GB", + "giga_byte_other": "{{count}} GB", + "kilo_byte_one": "{{count}} KB", + "kilo_byte_other": "{{count}} KB", + "mega_byte_one": "{{count}} MB", + "mega_byte_other": "{{count}} MB", + "tera_byte_one": "{{count}} TB", + "tera_byte_other": "{{count}} TB" + }, + "table_columns": { + "dimensions": "解像度", + "file_size": "サイズ", + "file_type": "種類", + "name": "名前" + }, + "title": "ダウンロード" + }, + "location": { + "title": "位置情報" + }, + "media": { + "album_path": "アルバムの場所", + "exif": { + "description": "EXIF 情報", + "exposure_program": { + "action_program": "アクション", + "aperture_priority": "絞り優先", + "bulb": "バルブ", + "creative_program": "クリエイティブ", + "landscape_mode": "風景", + "manual": "マニュアル", + "normal_program": "ノーマル", + "not_defined": "未定義", + "portrait_mode": "ポートレート", + "shutter_priority": "シャッター優先" + }, + "flash": { + "auto": "自動", + "did_not_fire": "発光なし", + "fired": "発光", + "no_flash": "発光禁止", + "no_flash_function": "フラッシュ非対応", + "off": "オフ", + "on": "オン", + "red_eye_reduction": "赤目軽減", + "return_detected": "反射光あり", + "return_not_detected": "反射光なし" + }, + "name": { + "aperture": "絞り値", + "camera": "カメラ名", + "coordinates": "位置情報", + "date_shot": "撮影日", + "exposure": "露出時間", + "exposure_program": "露出モード", + "flash": "フラッシュ", + "focal_length": "焦点距離", + "iso": "ISO感度", + "lens": "レンズ名", + "maker": "メーカー名" + } + } + }, + "people": { + "action_label": { + "detach_image": "顔グループの分割", + "merge_face": "顔グループの統合", + "move_face": "顔グループの移動" + }, + "confirm_image_detach": "この写真をこの顔グループから外しますか?", + "title": "人物" + }, + "sharing": { + "add_share": "共有を追加", + "copy_link": "リンクをコピー", + "delete": "削除", + "more": "その他", + "no_shares_found": "共有が見つかりません", + "public_link": "公開リンク", + "title": "共有オプション" + } + }, + "sidemenu": { + "albums": "アルバム", + "people": "人物", + "photos": "タイムライン", + "places": "場所", + "settings": "設定" + }, + "timeline_filter": { + "date": { + "dropdown_all": "すべて", + "dropdown_year": "{{year}}年以前", + "label": "日付" + } + }, + "title": { + "loading_album": "アルバムを読み込み中", + "login": "ログイン", + "people": "人物", + "settings": "設定" + } +} diff --git a/ui/src/extractedTranslations/nl/translation.json b/ui/src/extractedTranslations/nl/translation.json new file mode 100644 index 0000000..c8949dd --- /dev/null +++ b/ui/src/extractedTranslations/nl/translation.json @@ -0,0 +1,371 @@ +{ + "album_filter": { + "only_favorites": "Toon favorieten", + "sort": "Sorteren", + "sort_by": "Sorteren op", + "sort_direction": "Sorteerrichting", + "order_direction": { + "ascending": "Oplopend", + "descending": "Aflopend" + }, + "sorting_options": { + "date_imported": "Importdatum", + "date_shot": "Opnamedatum", + "title": "Titel", + "type": "Type" + } + }, + "general": { + "action": { + "add": "Toevoegen", + "cancel": "Annuleren", + "remove": "Verwijderen", + "save": "Opslaan" + }, + "loading": { + "album": "Album laden", + "default": "Laden...", + "page": "Pagina laden", + "paginate": { + "faces": "Meer personen laden", + "media": "Meer media laden" + }, + "shares": "Laden gedeelde albums..." + } + }, + "header": { + "search": { + "loading": "Resultaten laden...", + "no_results": "Geen resultaten gevonden", + "placeholder": "Zoeken", + "result_type": { + "albums": "Albums", + "media": "Media" + } + } + }, + "login_page": { + "field": { + "password": "Wachtwoord", + "submit": "Inloggen", + "username": "Gebruikersnaam" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Foto locatie", + "placeholder": "/pad/naar/foto's" + }, + "submit": "Photoview instellen" + }, + "title": "Initiële configuratie" + }, + "welcome": "Welkom bij Photoview" + }, + "meta": { + "description": "Eenvoudige en gebruiksvriendelijke fotogalerij voor zelf hosting" + }, + "people_page": { + "action_label": { + "change_label": "Label wijzigen", + "detach_images": "Gezicht loskoppelen", + "merge_face": "Gezicht samenvoegen", + "merge_people": "Gezichten samenvoegen", + "move_faces": "Gezichten verplaatsen" + }, + "face_group": { + "label_placeholder": "Label", + "unlabeled": "Zonder label", + "unlabeled_person": "Onbekende persoon" + }, + "modal": { + "action": { + "merge": "Samenvoegen", + "next": "Volgende" + }, + "detach_image_faces": { + "action": { + "detach": "Gezichten loskoppelen", + "select_images": "Selecteer afbeeldingen om los te koppelen" + }, + "description": "Koppel geselecteerde afbeeldingen los van deze gezichtsgroep en verplaats ze naar een nieuwe groep", + "title": "Gezichten loskoppelen" + }, + "merge_face_groups": { + "destination_description": "Selecteer de gezichtsgroep waarin andere groepen worden samengevoegd.", + "destination_table": { + "title": "Selecteer doelgezicht" + }, + "sources_description": "Selecteer alle gezichtsgroepen die samengevoegd worden met de doelgroep.", + "sources_table": { + "title": "Selecteer één of meer brongezichten:" + }, + "title": "Gezichtsgroepen samenvoegen" + }, + "move_image_faces": { + "description": "Verplaats geselecteerde afbeeldingen naar een andere gezichtsgroep", + "destination_face_group_table": { + "move_action": "Gezichten verplaatsen", + "title": "Selecteer doelgezichtsgroep" + }, + "image_select_table": { + "next_action": "Volgende", + "title": "Selecteer afbeeldingen" + }, + "title": "Gezichten verplaatsen" + } + }, + "recognize_unlabeled_faces_button": "Herken onbekende gezichten", + "tableselect_face_group": { + "search_faces_placeholder": "Zoek gezichten..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Zoek afbeeldingen..." + } + }, + "photos_page": { + "title": "Tijdlijn" + }, + "places_page": { + "title": "Locaties" + }, + "routes": { + "page_not_found": "Pagina niet gevonden" + }, + "settings": { + "concurrent_workers": { + "description": "Het maximale aantal scanprocessen dat tegelijk mag draaien", + "title": "Aantal gelijktijdige scans" + }, + "logout": "Uitloggen", + "periodic_scanner": { + "checkbox_label": "Periodieke scan inschakelen", + "field": { + "description": "Hoe vaak automatisch alle gebruikers gescand worden", + "label": "Scaninterval" + }, + "interval_unit": { + "days": "Dagen", + "hour": "Uur", + "minutes": "Minuten", + "months": "Maanden", + "seconds": "Seconden" + }, + "title": "Periodieke scanner" + }, + "scanner": { + "description": "Scant alle gebruikers op nieuwe of gewijzigde media", + "scan_all_users": "Scan alle gebruikers", + "title": "Scanner" + }, + "user_preferences": { + "change_language": { + "description": "Wijzig de taal voor deze gebruiker", + "label": "Website taal" + }, + "language_selector": { + "placeholder": "Selecteer taal" + }, + "theme": { + "auto": { + "label": "Zelfde als systeem" + }, + "dark": { + "label": "Donker" + }, + "description": "Wijzig het uiterlijk van de website", + "light": { + "label": "Licht" + }, + "title": "Thema-instellingen" + }, + "title": "Gebruikersvoorkeuren" + }, + "users": { + "add_user": { + "submit": "Gebruiker toevoegen" + }, + "confirm_delete_user": { + "action": "Verwijder {{user}}", + "description": "<0>Weet je zeker dat je <1> wilt verwijderen?

    Deze actie kan niet ongedaan worden gemaakt

    ", + "title": "Gebruiker verwijderen" + }, + "password_reset": { + "description": "Wijzig wachtwoord voor <1>{{username}}", + "form": { + "label": "Nieuw wachtwoord", + "placeholder": "wachtwoord", + "submit": "Wachtwoord wijzigen" + }, + "title": "Wachtwoord wijzigen" + }, + "table": { + "column_names": { + "action": "Actie", + "capabilities": "Rechten", + "photo_path": "Foto locatie", + "username": "Gebruikersnaam" + }, + "new_user": "Nieuwe gebruiker", + "row": { + "action": { + "change_password": "Wachtwoord wijzigen", + "delete": "Verwijderen", + "edit": "Bewerken", + "scan": "Scannen" + } + } + }, + "title": "Gebruikers" + }, + "version_info": { + "build_date_title": "Builddatum", + "title": "Photoview versie", + "version_title": "Releaseversie" + } + }, + "share_page": { + "media": { + "title": "Gedeelde media" + }, + "protected_share": { + "description": "Dit album is beveiligd met een wachtwoord.", + "password_required_error": "Wachtwoord is verplicht", + "title": "Beveiligd album" + }, + "share_not_found": "Album niet gevonden", + "share_not_found_description": "Misschien is het album verlopen of verwijderd.", + "wrong_password": "Onjuist wachtwoord, probeer opnieuw." + }, + "sidebar": { + "album": { + "album_cover": "Albumhoes", + "download": { + "high-resolutions": { + "description": "Hoge resolutie jpegs of RAW foto's", + "title": "Hoge resolutie" + }, + "originals": { + "description": "De originele foto's and video's", + "title": "Originelen" + }, + "thumbnails": { + "description": "Lage resolutie foto's, geen video's", + "title": "Thumbnails" + }, + "web-videos": { + "description": "Video's die geoptimaliseerd zijn voor web", + "title": "Geconverteerde video's" + } + }, + "reset_cover": "Omslagfoto resetten", + "set_cover": "Instellen als omslagfoto", + "title_placeholder": "Albumtitel" + }, + "download": { + "filesize": { + "byte_one": "{{count}} Byte", + "byte_other": "{{count}} Bytes", + "giga_byte_one": "{{count}} GB", + "giga_byte_other": "{{count}} GB", + "kilo_byte_one": "{{count}} KB", + "kilo_byte_other": "{{count}} KB", + "mega_byte_one": "{{count}} MB", + "mega_byte_other": "{{count}} MB", + "tera_byte_one": "{{count}} TB", + "tera_byte_other": "{{count}} TB" + }, + "table_columns": { + "dimensions": "Afmetingen", + "file_size": "Grootte", + "file_type": "Type", + "name": "Naam" + }, + "title": "Downloaden" + }, + "location": { + "title": "Locatie" + }, + "media": { + "album_path": "Album locatie", + "exif": { + "description": "Omschrijving", + "exposure_program": { + "action_program": "Actie programma", + "aperture_priority": "Diaframe prioriteit", + "bulb": "Bulb", + "creative_program": "Creatieve programma", + "landscape_mode": "Landschap mode", + "manual": "Handmatig", + "normal_program": "Normale programma", + "not_defined": "Not gedefinieerd", + "portrait_mode": "Portret mode", + "shutter_priority": "Sluitertijd prioriteit" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Niet afgegaan", + "fired": "Flitser afgegaan", + "no_flash": "Niet geflitst", + "no_flash_function": "Geen flits functie", + "off": "Uit", + "on": "Aan", + "red_eye_reduction": "Rode-ogenreductie", + "return_detected": "Flits gedetecteerd", + "return_not_detected": "Flits niet gedetecteerd" + }, + "name": { + "aperture": "Diafragma", + "camera": "Camera", + "coordinates": "Coördinaten", + "date_shot": "Opnamedatum", + "exposure": "Belichting", + "exposure_program": "Programma", + "flash": "Flitser", + "focal_length": "Brandpuntsafstand", + "iso": "ISO", + "lens": "Lens", + "maker": "Fabrikant" + } + } + }, + "people": { + "action_label": { + "detach_image": "Afbeelding loskoppelen", + "merge_face": "Gezicht samenvoegen", + "move_face": "Gezicht verplaatsen" + }, + "confirm_image_detach": "Weet je zeker dat je deze afbeelding wilt loskoppelen?", + "title": "Personen" + }, + "sharing": { + "add_share": "Share toevoegen", + "copy_link": "Link kopiëren", + "delete": "Verwijderen", + "more": "Meer", + "no_shares_found": "Geen albums gevonden", + "public_link": "Publieke link", + "title": "Deelopties" + } + }, + "sidemenu": { + "albums": "Albums", + "people": "Personen", + "photos": "Tijdlijn", + "places": "Locaties", + "settings": "Instellingen" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Vanaf vandaag", + "dropdown_year": "{{year}} en eerder", + "label": "Datum" + } + }, + "title": { + "loading_album": "Album laden", + "login": "Inloggen", + "people": "Personen", + "settings": "Instellingen" + } +} diff --git a/ui/src/extractedTranslations/pl/translation.json b/ui/src/extractedTranslations/pl/translation.json new file mode 100644 index 0000000..96b91a6 --- /dev/null +++ b/ui/src/extractedTranslations/pl/translation.json @@ -0,0 +1,381 @@ +{ + "album_filter": { + "only_favorites": "Pokaż tylko ulubione", + "sort": "Sortuj", + "sort_by": "Sortuj", + "sort_direction": "Kierunek sortowania", + "order_direction": { + "ascending": "Rosnąco", + "descending": "Malejąco" + }, + "sorting_options": { + "date_imported": "Data zaimportowania", + "date_shot": "Data wykonania", + "title": "Tytuł", + "type": "Rodzaj" + } + }, + "general": { + "action": { + "add": "Dodaj", + "cancel": "Anuluj", + "remove": "Usuń", + "save": "Zapisz" + }, + "loading": { + "album": "Ładowanie albumu", + "default": "Ładowanie...", + "page": "Ładowanie strony", + "paginate": { + "faces": "Ładowanie kolejnych osób", + "media": "Ładowanie kolejnych mediów" + }, + "shares": "Ładowanie udostępnionych..." + } + }, + "header": { + "search": { + "loading": "Rezultaty ładowania...", + "no_results": "Nie znaleziono wyników", + "placeholder": "Szukaj", + "result_type": { + "albums": "Albumy", + "media": "Multimedia" + } + } + }, + "login_page": { + "field": { + "password": "Hasło", + "submit": "Zaloguj się", + "username": "Nazwa użytkownika" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Ścieżka zdjęcia", + "placeholder": "/sciezka/do/zdjec" + }, + "submit": "Konfiguracja Photoview" + }, + "title": "Konfiguracja początkowa" + }, + "welcome": "Witaj w Photoview" + }, + "meta": { + "description": "Prosta i przyjazna dla użytkownika galeria zdjęć" + }, + "people_page": { + "action_label": { + "change_label": "Zmień etykietę", + "detach_images": "Odłącz twarz", + "merge_face": "", + "merge_people": "Połącz twarze", + "move_faces": "Przenieś twarze" + }, + "face_group": { + "label_placeholder": "Etykieta", + "unlabeled": "Nieoznakowany", + "unlabeled_person": "Nieoznakowana osoba" + }, + "modal": { + "action": { + "merge": "Połącz", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "Odłącz zdjęcia twarzy", + "select_images": "Zaznacz zdjęcia do odłączenia" + }, + "description": "Odłącz wybrane zdjęcia w tej grupie twarzy i przenieś je do nowej grupy twarzy", + "title": "Odłącz zdjęcia twarzy" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "Wybierz twarz" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "Połącz grupy twarzy" + }, + "move_image_faces": { + "description": "Przenieś wybrane obrazy tej grupy twarzy do innej grupy twarzy", + "destination_face_group_table": { + "move_action": "Przenieś twarze z obrazu", + "title": "Wybierz docelową grupę twarzy" + }, + "image_select_table": { + "next_action": "Dalej", + "title": "Wybierz obrazy do przeniesienia" + }, + "title": "Przenieś twarze z obrazu" + } + }, + "recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze", + "tableselect_face_group": { + "search_faces_placeholder": "Szukaj twarzy..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Szukaj obrazów..." + } + }, + "photos_page": { + "title": "Oś czasu" + }, + "places_page": { + "title": "Miejsca" + }, + "routes": { + "page_not_found": "Nie znaleziono strony" + }, + "settings": { + "concurrent_workers": { + "description": "Maksymalna liczba zadań skanera, które mogą być uruchomione jednocześnie", + "title": "Liczba zadań skanera" + }, + "logout": "Wyloguj", + "periodic_scanner": { + "checkbox_label": "Włącz skaner okresowy", + "field": { + "description": "Jak często skaner powinien wykonywać automatyczne skanowanie dla wszystkich użytkowników", + "label": "Interwał skanowania okresowego" + }, + "interval_unit": { + "days": "Dni", + "hour": "Godzin", + "minutes": "Minut", + "months": "Miesięcy", + "seconds": "Sekund" + }, + "title": "Skaner okresowy" + }, + "scanner": { + "description": "Skanuje wszystkich użytkowników w poszukiwaniu nowych lub zaktualizowanych mediów", + "scan_all_users": "Przeskanuj wszystkich użytkowników", + "title": "Skaner" + }, + "user_preferences": { + "change_language": { + "description": "Zmień język strony dla tego użytkownika", + "label": "Język witryny" + }, + "language_selector": { + "placeholder": "Wybierz język" + }, + "theme": { + "auto": { + "label": "Taki jak system" + }, + "dark": { + "label": "Ciemny" + }, + "description": "Zmień wygląd strony", + "light": { + "label": "Jasny" + }, + "title": "Motyw" + }, + "title": "Preferencje użytkownika" + }, + "users": { + "add_user": { + "submit": "Dodaj użytkownika" + }, + "confirm_delete_user": { + "action": "Usuń {{user}}", + "description": "<0>Czy jesteś pewien, że chcesz usunąć <1>?

    Tej czynności nie można cofnąć

    ", + "title": "Usuń użytkownika" + }, + "password_reset": { + "description": "Zmiana hasła dla <1>{{username}}", + "form": { + "label": "Nowe hasło", + "placeholder": "hasło", + "submit": "Zmień hasło" + }, + "title": "Zmień hasło" + }, + "table": { + "column_names": { + "action": "Akcja", + "capabilities": "Możliwości", + "photo_path": "Ścieżka zdjęć", + "username": "Nazwa użytkownika" + }, + "new_user": "Nowy użytkownik", + "row": { + "action": { + "change_password": "Zmień hasła", + "delete": "Usuń", + "edit": "Edytuj", + "scan": "Skanuj" + } + } + }, + "title": "Użytkownicy" + }, + "version_info": { + "build_date_title": "Data kompilacji", + "title": "Wersja Photoview", + "version_title": "Wersja produkcyjna" + } + }, + "share_page": { + "media": { + "title": "Udostępnione" + }, + "protected_share": { + "description": "Ten udział jest chroniony hasłem.", + "password_required_error": "Hasło jest wymagane", + "title": "Udział chroniony" + }, + "share_not_found": "Nie znaleziono udziału", + "share_not_found_description": "Może udział wygasł lub został usunięty.", + "wrong_password": "Błędne hasło, proszę spróbować ponownie." + }, + "sidebar": { + "album": { + "album_cover": "Okładka albumu", + "download": { + "high-resolutions": { + "description": "Wysokiej rozdzielczości zdjęcia typu jpeg z obrazów RAW", + "title": "Wysokie rozdzielczości" + }, + "originals": { + "description": "Oryginalne zdjęcia i filmy", + "title": "Oryginały" + }, + "thumbnails": { + "description": "Zdjęcia o niskiej rozdzielczości, bez filmów", + "title": "Miniatury" + }, + "web-videos": { + "description": "Filmy zoptymalizowane pod kątem sieci", + "title": "Przekonwertowane filmy" + } + }, + "reset_cover": "Zresetuj zdjęcie okładki", + "set_cover": "Ustaw jako zdjęcie okładki albumu", + "title_placeholder": "Tytuł albumu" + }, + "download": { + "filesize": { + "byte_one": "{{count}} bajt", + "byte_few": "", + "byte_many": "", + "byte_other": "{{count}} bajty", + "giga_byte_one": "", + "giga_byte_few": "", + "giga_byte_many": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_few": "", + "kilo_byte_many": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_few": "", + "mega_byte_many": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_few": "", + "tera_byte_many": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Wymiary", + "file_size": "Rozmiar", + "file_type": "Typ", + "name": "Nazwa" + }, + "title": "Pobierz" + }, + "location": { + "title": "Lokalizacja" + }, + "media": { + "album_path": "Ścieżka albumu", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Program działania", + "aperture_priority": "Priorytet przysłony", + "bulb": "Lampa", + "creative_program": "Program kreatywny", + "landscape_mode": "Tryb krajobrazowy", + "manual": "Ręczny", + "normal_program": "Normalny program", + "not_defined": "Nieokreślony", + "portrait_mode": "Tryb portretowy", + "shutter_priority": "Priorytet migawki" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Bez błysku", + "fired": "Błysk", + "no_flash": "Bez lampy", + "no_flash_function": "Brak lampy błyskowej", + "off": "Wyłączona", + "on": "Włączona", + "red_eye_reduction": "Redukcja efektu czerwonych oczu", + "return_detected": "Wykryto powrót", + "return_not_detected": "Nie wykryto powrotu" + }, + "name": { + "aperture": "Przysłona", + "camera": "Aparat ", + "coordinates": "", + "date_shot": "Data wykonania", + "exposure": "Ekspozycja", + "exposure_program": "Program", + "flash": "Lampa", + "focal_length": "Ogniskowa", + "iso": "ISO", + "lens": "Obiektyw", + "maker": "Twórca" + } + } + }, + "people": { + "action_label": { + "detach_image": "Odłącz zdjęcie", + "merge_face": "Połącz twarz", + "move_face": "Przesuń twarz" + }, + "confirm_image_detach": "Czy na pewno chcesz odłączyć to zdjęcie?", + "title": "Ludzie" + }, + "sharing": { + "add_share": "Dodaj udział", + "copy_link": "Skopiuj link", + "delete": "Usuń", + "more": "Więcej", + "no_shares_found": "Nie znaleziono udostępnionych", + "public_link": "Link publiczny", + "title": "Opcje udostępniania" + } + }, + "sidemenu": { + "albums": "Albumy", + "people": "Ludzie", + "photos": "Zdjęcia", + "places": "Miejsca", + "settings": "Ustawienia" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Od dziś", + "dropdown_year": "{{year}} i wcześniej", + "label": "Data" + } + }, + "title": { + "loading_album": "Ładowanie albumu", + "login": "Logowanie", + "people": "Ludzie", + "settings": "Ustawienia" + } +} diff --git a/ui/src/extractedTranslations/pl/translation_old.json b/ui/src/extractedTranslations/pl/translation_old.json new file mode 100644 index 0000000..c7bcb1e --- /dev/null +++ b/ui/src/extractedTranslations/pl/translation_old.json @@ -0,0 +1,184 @@ +{ + "album_filter": { + "sort": null + }, + "albums_page": { + "title": "Albumy" + }, + "general": { + "action": { + "delete": "Usuń", + "more": "Więcej" + }, + "loading": { + "media": "Ładowanie mediów", + "timeline": "Ładowanie osi czasu" + } + }, + "header": { + "search": { + "result_type": { + "media": null, + "photos": "Zdjęcia" + } + } + }, + "people_page": { + "action_label": { + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "unlabeled_person": null + }, + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": "Wszystkie obrazy w tej grupie twarzy zostaną połączone z wybraną grupą twarzy.", + "destination_table": { + "title": null + }, + "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + }, + "tableselect_face_group": { + "search_faces_placeholder": null + }, + "tableselect_image_faces": { + "search_images_placeholder": null + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Admin", + "capabilities": null + } + } + }, + "version_info": { + "build_date_title": null, + "title": null, + "version_title": null + } + }, + "share_page": { + "protected_share": { + "password_required_error": null + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "Opcje albumu", + "title_placeholder": null + }, + "download": { + "filesize": { + "byte": "{{count}} Bajt", + "byte_0": "", + "byte_1": "", + "byte_2": "", + "byte_plural": "{{count}} Bajtów", + "giga_byte": "{{count}} GB", + "giga_byte_0": "", + "giga_byte_1": "", + "giga_byte_2": "", + "kilo_byte": "{{count}} KB", + "kilo_byte_0": "", + "kilo_byte_1": "", + "kilo_byte_2": "", + "mega_byte": "{{count}} MB", + "mega_byte_0": "", + "mega_byte_1": "", + "mega_byte_2": "", + "tera_byte": "{{count}} TB", + "tera_byte_0": "", + "tera_byte_1": "", + "tera_byte_2": "" + } + }, + "media": { + "album": "" + }, + "sharing": { + "delete": null, + "more": null, + "table_header": "Publiczne udostępnienia" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + }, + "title": { + "login": null + } +} diff --git a/ui/src/extractedTranslations/pt/translation.json b/ui/src/extractedTranslations/pt/translation.json new file mode 100644 index 0000000..0c51357 --- /dev/null +++ b/ui/src/extractedTranslations/pt/translation.json @@ -0,0 +1,376 @@ +{ + "album_filter": { + "only_favorites": "Mostrar apenas os favoritos", + "sort": "Ordenar", + "sort_by": "Ordenado por", + "sort_direction": "Sentido da ordenação", + "order_direction": { + "ascending": "Ascendente", + "descending": "Decrescente" + }, + "sorting_options": { + "date_imported": "Data de importação", + "date_shot": "Data da foto", + "title": "Título", + "type": "Tipo" + } + }, + "general": { + "action": { + "add": "Adicionar", + "cancel": "Cancelar", + "remove": "Remover", + "save": "Guardar" + }, + "loading": { + "album": "Carregar Álbum", + "default": "Carregar...", + "page": "A carregar a página", + "paginate": { + "faces": "A carregar mais pessoas", + "media": "A carregar mais mídia" + }, + "shares": "A carregar partilhas..." + } + }, + "header": { + "search": { + "loading": "A carregar resultados...", + "no_results": "Nenhum resultado encontrado", + "placeholder": "Procurar", + "result_type": { + "albums": "Álbuns", + "media": "Mídia" + } + } + }, + "login_page": { + "field": { + "password": "Palavra-passe", + "submit": "Entrar", + "username": "Nome de utilizador" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Caminho para fotos", + "placeholder": "/caminho/para/fotos" + }, + "submit": "Configurações Iniciais" + }, + "title": "Configurações Iniciais" + }, + "welcome": "Bem-vindo ao Photoview" + }, + "meta": { + "description": "Galeria de fotos simples e fácil de usar para servidores pessoais" + }, + "people_page": { + "action_label": { + "change_label": "", + "detach_images": "", + "merge_face": "", + "merge_people": "", + "move_faces": "" + }, + "face_group": { + "label_placeholder": "Etiqueta", + "unlabeled": "Sem etiqueta", + "unlabeled_person": "Pessoa sem etiqueta" + }, + "modal": { + "action": { + "merge": "Juntar", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "Desanexar caras de imagem", + "select_images": "Selecione imagens para desanexar" + }, + "description": "Desanexe as imagens selecionadas deste grupo de caras e mova-as para um novo grupo", + "title": "Desanexar caras de imagens" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "Selecione a cara de destino" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "Juntar grupos de cara" + }, + "move_image_faces": { + "description": "Mover as imagens selecionadas deste grupo de caras para outro grupo", + "destination_face_group_table": { + "move_action": "Mover caras de imagens", + "title": "Selecione o grupo de caras destino" + }, + "image_select_table": { + "next_action": "Próxima", + "title": "Selecionar imagens para mover" + }, + "title": "Mover caras de imagens" + } + }, + "recognize_unlabeled_faces_button": "Reconhecer caras sem etiqueta", + "tableselect_face_group": { + "search_faces_placeholder": "Procurar caras..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Procurar imagens..." + } + }, + "photos_page": { + "title": "Fotos" + }, + "places_page": { + "title": "Locais" + }, + "routes": { + "page_not_found": "Página não encontrada" + }, + "settings": { + "concurrent_workers": { + "description": "Quantidade máxima de processos de procura que podem ser executados de uma vez só", + "title": "Processos simultâneos para procura" + }, + "logout": "Sair", + "periodic_scanner": { + "checkbox_label": "Ativar procura automática", + "field": { + "description": "Com que frequência a procura deve ser executada de forma automática para todos os utilizadores", + "label": "Intervalo periódico para procura automática" + }, + "interval_unit": { + "days": "Dias", + "hour": "Horas", + "minutes": "Minutos", + "months": "Meses", + "seconds": "Segundos" + }, + "title": "Procurar periodicamente" + }, + "scanner": { + "description": "Irá procurar todos os utilizadores para encontrar mídia nova ou atualizada", + "scan_all_users": "Procurar todos os utilizadores", + "title": "Procurar fotos dos utilizadores" + }, + "user_preferences": { + "change_language": { + "description": "Alterar o idioma do website específico para este utilizador", + "label": "Idioma do website" + }, + "language_selector": { + "placeholder": "Selecionar Idioma" + }, + "theme": { + "auto": { + "label": "" + }, + "dark": { + "label": "" + }, + "description": "", + "light": { + "label": "" + }, + "title": "" + }, + "title": "Preferências do utilizador" + }, + "users": { + "add_user": { + "submit": "Adicionar utilizador" + }, + "confirm_delete_user": { + "action": "Apagar {{user}}", + "description": "<0>Tem a certeza de que pretende apagar <1>?

    Esta ação não pode ser desfeita.

    ", + "title": "Apagar utilizador" + }, + "password_reset": { + "description": "Mudar palavra-passe para <1>{{username}}", + "form": { + "label": "Nova palavra-passe", + "placeholder": "palavra-passe", + "submit": "Mudar palavra-passe" + }, + "title": "Mudar palavra-passe" + }, + "table": { + "column_names": { + "action": "Ação", + "capabilities": "Privilégios", + "photo_path": "Caminho para as fotos", + "username": "Nome do Utilizador" + }, + "new_user": "Novo utilizador", + "row": { + "action": { + "change_password": "Mudar palavra-passe", + "delete": "Eliminar", + "edit": "Editar", + "scan": "Procurar" + } + } + }, + "title": "Utilizadores" + }, + "version_info": { + "build_date_title": "Data da compilação", + "title": "Versão do Photoview ", + "version_title": "Versão Release" + } + }, + "share_page": { + "media": { + "title": "Mídia partilhada" + }, + "protected_share": { + "description": "Este partilha está protegida por senha.", + "password_required_error": "Palavra-passe é obrigatória", + "title": "Partilha protegida" + }, + "share_not_found": "Partilha não encontrada", + "share_not_found_description": "Talvez a partilha tenha expirado ou sido removida.", + "wrong_password": "Palavra-passe errada, por favor tente novamente." + }, + "sidebar": { + "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", + "title_placeholder": "Título do Álbum" + }, + "download": { + "filesize": { + "byte_one": "", + "byte_many": "", + "byte_other": "", + "giga_byte_one": "", + "giga_byte_many": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_many": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_many": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_many": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Dimensões", + "file_size": "Tamanho", + "file_type": "Tipo", + "name": "Nome" + }, + "title": "Descarregar" + }, + "location": { + "title": "" + }, + "media": { + "album_path": "", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Programa de ação", + "aperture_priority": "Prioridade da abertura", + "bulb": "Lâmpada", + "creative_program": "Programa criativo", + "landscape_mode": "Modo paisagem", + "manual": "Manual", + "normal_program": "Programa Normal", + "not_defined": "Não definido", + "portrait_mode": "Modo Retrato", + "shutter_priority": "Prioridade do obturador" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Não disparou", + "fired": "Disparou", + "no_flash": "Sem Flash", + "no_flash_function": "Sem função de flash", + "off": "Desligado", + "on": "Ligado", + "red_eye_reduction": "Redução de olhos vermelhos", + "return_detected": "Retorno detetado", + "return_not_detected": "Retorno não detetado" + }, + "name": { + "aperture": "Abertura", + "camera": "Câmera", + "coordinates": "", + "date_shot": "Data da foto", + "exposure": "Exposição", + "exposure_program": "Programa", + "flash": "Flash", + "focal_length": "Distância Focal", + "iso": "ISO", + "lens": "Lente", + "maker": "Fabricante" + } + } + }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "confirm_image_detach": "", + "title": "" + }, + "sharing": { + "add_share": "Adicionar partilha", + "copy_link": "Copiar link", + "delete": "Eliminar", + "more": "Mais", + "no_shares_found": "Não foram encontradas partilhas", + "public_link": "Link público", + "title": "Opções de partilha" + } + }, + "sidemenu": { + "albums": "Álbuns", + "people": "Pessoas", + "photos": "Fotos", + "places": "Locais", + "settings": "Configurações" + }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, + "title": { + "loading_album": "Carregar Álbum", + "login": "Login", + "people": "Pessoas", + "settings": "Configurações" + } +} diff --git a/ui/src/extractedTranslations/pt/translation_old.json b/ui/src/extractedTranslations/pt/translation_old.json new file mode 100644 index 0000000..b53f9f7 --- /dev/null +++ b/ui/src/extractedTranslations/pt/translation_old.json @@ -0,0 +1,55 @@ +{ + "people_page": { + "action_label": { + "change_label": null, + "detach_images": null, + "merge_people": null, + "move_faces": null + }, + "modal": { + "merge_face_groups": { + "description": "Todas as imagens neste grupo de caras serão juntas no grupo selecionado." + }, + "merge_people_groups": { + "description": "Todas as imagens neste grupo de caras serão juntas no grupo selecionado.", + "destination_table": { + "title": "Selecione a cara de destino" + }, + "title": "Juntar grupos de cara" + } + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + } + }, + "sidebar": { + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "{{count}} GB", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "{{count}} KB", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "{{count}} MB", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "{{count}} TB" + } + } + } +} diff --git a/ui/src/extractedTranslations/ru/translation.json b/ui/src/extractedTranslations/ru/translation.json new file mode 100644 index 0000000..3ab2f96 --- /dev/null +++ b/ui/src/extractedTranslations/ru/translation.json @@ -0,0 +1,381 @@ +{ + "album_filter": { + "only_favorites": "Показать только избранные", + "sort": "Сортировка", + "sort_by": "Отсортировать по", + "sort_direction": "Направление сортировки", + "order_direction": { + "ascending": "По возрастанию", + "descending": "По убыванию" + }, + "sorting_options": { + "date_imported": "Дата импортирования", + "date_shot": "Дата снимка", + "title": "Заголовок", + "type": "Вид" + } + }, + "general": { + "action": { + "add": "Добавить", + "cancel": "Отменить", + "remove": "Убрать", + "save": "Сохранить" + }, + "loading": { + "album": "Загрузка альбома", + "default": "Загрузка...", + "page": "Загрузка страницы", + "paginate": { + "faces": "Загрузить еще людей", + "media": "Загрузить еще медиа" + }, + "shares": "Загрузка доступа..." + } + }, + "header": { + "search": { + "loading": "Загрузка результатов...", + "no_results": "Результатов не найдено", + "placeholder": "Поиск", + "result_type": { + "albums": "Альбомы", + "media": "" + } + } + }, + "login_page": { + "field": { + "password": "Пароль", + "submit": "Войти", + "username": "Имя пользователя" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Путь к фото", + "placeholder": "/path/to/photos" + }, + "submit": "Настроить Photoview" + }, + "title": "Первичная настройка" + }, + "welcome": "Добро пожаловать в Photoview" + }, + "meta": { + "description": "Простая и Удобная Фото Галерея для Личного Сервера" + }, + "people_page": { + "action_label": { + "change_label": "", + "detach_images": "", + "merge_face": "", + "merge_people": "", + "move_faces": "" + }, + "face_group": { + "label_placeholder": "Метка", + "unlabeled": "Без метки", + "unlabeled_person": "Человек без метки" + }, + "modal": { + "action": { + "merge": "Объединить", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "Отсоединить фото с лицами", + "select_images": "Выбрать фото для отсоединения" + }, + "description": "Отсоединить выбранные фотографии с лицами из текущей группы и переместить их в новую группу.", + "title": "Отсоединить фото с лицами" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "Выберете конечное лицо" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "Обьединить группы лиц" + }, + "move_image_faces": { + "description": "Переместить выбранные фото из текущей группы в другую группу с лицами.", + "destination_face_group_table": { + "move_action": "Переместить фото с лицами", + "title": "Выберите конечную группу с лицами" + }, + "image_select_table": { + "next_action": "Далее", + "title": "Выберите фото для перемещения" + }, + "title": "Переместить фото с лицами" + } + }, + "recognize_unlabeled_faces_button": "Распознавать непомеченные лица", + "tableselect_face_group": { + "search_faces_placeholder": "" + }, + "tableselect_image_faces": { + "search_images_placeholder": "" + } + }, + "photos_page": { + "title": "Фотографии" + }, + "places_page": { + "title": "Места" + }, + "routes": { + "page_not_found": "Страница не найдена" + }, + "settings": { + "concurrent_workers": { + "description": "Максимальное количество задач по сканированию разрешённых работать одновременно", + "title": "Количество одновременных задач" + }, + "logout": "Выйти", + "periodic_scanner": { + "checkbox_label": "Включить переодическое сканирование", + "field": { + "description": "Как часто должно запускаться автоматическое сканирование для всех пользователей", + "label": "Интервал переодического сканирования" + }, + "interval_unit": { + "days": "День", + "hour": "Час", + "minutes": "Минута", + "months": "Месяц", + "seconds": "Секунда" + }, + "title": "Переодическое сканирование" + }, + "scanner": { + "description": "Сканирование всех пользователей на наличие обновлённых или новых медиа", + "scan_all_users": "Сканировать всех пользователей", + "title": "Сканер" + }, + "user_preferences": { + "change_language": { + "description": "Поменять язык сайта только для этого пользователя", + "label": "Язык сайта" + }, + "language_selector": { + "placeholder": "Выбрать язык" + }, + "theme": { + "auto": { + "label": "" + }, + "dark": { + "label": "" + }, + "description": "", + "light": { + "label": "" + }, + "title": "" + }, + "title": "Пользовательские настройки" + }, + "users": { + "add_user": { + "submit": "Добавить пользователя" + }, + "confirm_delete_user": { + "action": "Удалить {{user}}", + "description": "<0>Вы уверены что хотите удалить <1>?

    Это действие необратимо

    ", + "title": "Удалить пользователя" + }, + "password_reset": { + "description": "Поменять пароль для <1>", + "form": { + "label": "Новый пароль", + "placeholder": "пароль", + "submit": "Поменять пароль" + }, + "title": "Поменять пароль" + }, + "table": { + "column_names": { + "action": "Действие", + "capabilities": "", + "photo_path": "Путь к фото", + "username": "Имя пользователя" + }, + "new_user": "Новый пользователь", + "row": { + "action": { + "change_password": "Поменять пароль", + "delete": "Удалить", + "edit": "Редактировать", + "scan": "Сканировать" + } + } + }, + "title": "Пользователи" + }, + "version_info": { + "build_date_title": "Дата сборки", + "title": "Версия Photoview", + "version_title": "Версия релиза" + } + }, + "share_page": { + "media": { + "title": "Общее медиа" + }, + "protected_share": { + "description": "Это общее медиа защищено паролем.", + "password_required_error": "", + "title": "Защищённое медиа" + }, + "share_not_found": "Общее медиа не найдено", + "share_not_found_description": "Возможно это общее медиа было удалено или время доступа истекло.", + "wrong_password": "Неправильный пароль, пожалуйста попробуйте снова." + }, + "sidebar": { + "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", + "title_placeholder": "" + }, + "download": { + "filesize": { + "byte_one": "", + "byte_few": "", + "byte_many": "", + "byte_other": "", + "giga_byte_one": "", + "giga_byte_few": "", + "giga_byte_many": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_few": "", + "kilo_byte_many": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_few": "", + "mega_byte_many": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_few": "", + "tera_byte_many": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Габариты", + "file_size": "Размер", + "file_type": "Тип", + "name": "Имя" + }, + "title": "Скачать" + }, + "location": { + "title": "" + }, + "media": { + "album_path": "", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Действие программа", + "aperture_priority": "Приоритет диафрагмы", + "bulb": "Лампа", + "creative_program": "Творческая программа", + "landscape_mode": "Ландшафтный режим", + "manual": "Ручной", + "normal_program": "Нормальная программа", + "not_defined": "Не определен", + "portrait_mode": "Портретный режим", + "shutter_priority": "Приоритет выдержки" + }, + "flash": { + "auto": "Авто", + "did_not_fire": "Не сработала", + "fired": "Сработала", + "no_flash": "Без вспышки", + "no_flash_function": "Функция без вспышки", + "off": "Выкл.", + "on": "Вкл.", + "red_eye_reduction": "Уменьшение эф. красных глаз", + "return_detected": "Обнаружен возврат", + "return_not_detected": "Возврат не обнаружен" + }, + "name": { + "aperture": "Диафрагма", + "camera": "Камера", + "coordinates": "", + "date_shot": "Дата снимка", + "exposure": "Экспозиция", + "exposure_program": "Программа", + "flash": "Вспышка", + "focal_length": "Фокусное расстояние", + "iso": "ISO", + "lens": "Линза", + "maker": "Производитель" + } + } + }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "confirm_image_detach": "", + "title": "" + }, + "sharing": { + "add_share": "Поделится", + "copy_link": "Скопировать ссылку", + "delete": "", + "more": "", + "no_shares_found": "Нет доступа", + "public_link": "Общедоступная ссылка", + "title": "Настройки доступа" + } + }, + "sidemenu": { + "albums": "Альбомы", + "people": "Люди", + "photos": "Фото", + "places": "Места", + "settings": "Настройки" + }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, + "title": { + "loading_album": "Загрузка альбома", + "login": "Имя пользователя", + "people": "Люди", + "settings": "Настройки" + } +} diff --git a/ui/src/extractedTranslations/ru/translation_old.json b/ui/src/extractedTranslations/ru/translation_old.json new file mode 100644 index 0000000..5d0a089 --- /dev/null +++ b/ui/src/extractedTranslations/ru/translation_old.json @@ -0,0 +1,148 @@ +{ + "album_filter": { + "sort": null + }, + "albums_page": { + "title": "Альбомы" + }, + "general": { + "action": { + "delete": "Удалить", + "more": "Более" + }, + "loading": { + "media": "Загрузка медиа", + "timeline": "Загрузка хронологии" + } + }, + "header": { + "search": { + "result_type": { + "media": null, + "photos": "Фото" + } + } + }, + "people_page": { + "action_label": { + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "face_group": { + "action": { + "add_label": "Добавить Метку", + "change_label": "Поменять Метку", + "detach_face": "Отсоединить Лицо", + "merge_face": "Объединить Лицо", + "move_faces": "Переместить Лица" + } + }, + "modal": { + "merge_face_groups": { + "description": "Все фотографии в этой группе лиц будут обьеденены с выбранной группой." + }, + "merge_people_groups": { + "description": "Все фотографии в этой группе лиц будут обьеденены с выбранной группой.", + "destination_table": { + "title": "Выберете конечное лицо" + }, + "title": "Обьединить группы лиц" + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": "Поиск лиц..." + }, + "select_image_faces": { + "search_images_placeholder": "Поиск фото..." + } + }, + "tableselect_face_group": { + "search_faces_placeholder": null + }, + "tableselect_image_faces": { + "search_images_placeholder": null + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Администрирование", + "capabilities": null + } + } + } + }, + "share_page": { + "protected_share": { + "password_required_error": null + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "Свойства альбома", + "title_placeholder": null + }, + "download": { + "filesize": { + "byte": "{{count}} Байт", + "byte_0": "", + "byte_1": "", + "byte_2": "", + "byte_plural": "{{count}} Байт", + "giga_byte": "{{count}} ГБ", + "giga_byte_0": "", + "giga_byte_1": "", + "giga_byte_2": "", + "kilo_byte": "{{count}} КБ", + "kilo_byte_0": "", + "kilo_byte_1": "", + "kilo_byte_2": "", + "mega_byte": "{{count}} МБ", + "mega_byte_0": "", + "mega_byte_1": "", + "mega_byte_2": "", + "tera_byte": "{{count}} ТБ", + "tera_byte_0": "", + "tera_byte_1": "", + "tera_byte_2": "" + } + }, + "media": { + "album": "" + }, + "sharing": { + "delete": null, + "more": null, + "table_header": "Общий доступ" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + } +} diff --git a/ui/src/extractedTranslations/sv/translation.json b/ui/src/extractedTranslations/sv/translation.json new file mode 100644 index 0000000..3a5f5e8 --- /dev/null +++ b/ui/src/extractedTranslations/sv/translation.json @@ -0,0 +1,371 @@ +{ + "album_filter": { + "only_favorites": "Visa endast favoriter", + "sort": "Sortering", + "sort_by": "Sortera efter", + "sort_direction": "Sorteringsriktning", + "order_direction": { + "ascending": "Stigande", + "descending": "Fallande" + }, + "sorting_options": { + "date_imported": "Datum för import", + "date_shot": "Datum", + "title": "Titel", + "type": "Typ" + } + }, + "general": { + "action": { + "add": "Lägg till", + "cancel": "Avbryt", + "remove": "Ta bort", + "save": "Spara" + }, + "loading": { + "album": "Laddar album", + "default": "Laddar...", + "page": "Laddar sida", + "paginate": { + "faces": "Laddar fler personer", + "media": "Laddar fler media" + }, + "shares": "Loading delningar..." + } + }, + "header": { + "search": { + "loading": "Laddar resultat...", + "no_results": "Inga resultat hittades", + "placeholder": "Sök", + "result_type": { + "albums": "Album", + "media": "" + } + } + }, + "login_page": { + "field": { + "password": "Lösenord", + "submit": "Logga in", + "username": "Användarnamn" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Sökväg till bild", + "placeholder": "/sökväg/till/bilder" + }, + "submit": "Uppstart Photoview" + }, + "title": "Första Uppstart" + }, + "welcome": "Välkommen till Photoview" + }, + "meta": { + "description": "Enkelt och Användarvänligt fotogalleri för personliga servrar" + }, + "people_page": { + "action_label": { + "change_label": "Byt märkning", + "detach_images": "Skilj ansikte", + "merge_face": "Slå ihop ansikte", + "merge_people": "Slå ihop ansikten", + "move_faces": "Flytta ansikten" + }, + "face_group": { + "label_placeholder": "Märkning", + "unlabeled": "Omärkt", + "unlabeled_person": "Omärkt person" + }, + "modal": { + "action": { + "merge": "Slå ihop", + "next": "Nästa" + }, + "detach_image_faces": { + "action": { + "detach": "", + "select_images": "" + }, + "description": "", + "title": "" + }, + "merge_face_groups": { + "destination_description": "Välj målansiktsgruppen som andra ansikten ska slås samman med.", + "destination_table": { + "title": "Välj målansiktet" + }, + "sources_description": "Välj en eller flera ansiktsgrupper att slå samman med målgruppen", + "sources_table": { + "title": "Välj minst ett ansikte att slå ihop med:" + }, + "title": "Slå Ihop Ansiktsgrupper" + }, + "move_image_faces": { + "description": "", + "destination_face_group_table": { + "move_action": "", + "title": "" + }, + "image_select_table": { + "next_action": "", + "title": "" + }, + "title": "" + } + }, + "recognize_unlabeled_faces_button": "Identifiera omärkta ansikten", + "tableselect_face_group": { + "search_faces_placeholder": "" + }, + "tableselect_image_faces": { + "search_images_placeholder": "" + } + }, + "photos_page": { + "title": "Bilder" + }, + "places_page": { + "title": "" + }, + "routes": { + "page_not_found": "Sidan hittades inte" + }, + "settings": { + "concurrent_workers": { + "description": "Max antal skannerjobb som får köras sammtidigt", + "title": "Samtidiga skannerarbetare" + }, + "logout": "Logga ut", + "periodic_scanner": { + "checkbox_label": "Aktivera periodisk skanner", + "field": { + "description": "Hur ofta ska skannern utföra automatiska skanningar av alla användare", + "label": "Periodiskt skanningsintervall" + }, + "interval_unit": { + "days": "Dagar", + "hour": "Timmar", + "minutes": "Minuter", + "months": "Månader", + "seconds": "Sekunder" + }, + "title": "Periodisk skanner" + }, + "scanner": { + "description": "Ska skanna alla användare efter nya eller uppdaterade media ", + "scan_all_users": "Skanna alla användare", + "title": "Skanner" + }, + "user_preferences": { + "change_language": { + "description": "Andrä språk specifikt för den här användaren", + "label": "Språk" + }, + "language_selector": { + "placeholder": "Välj språk" + }, + "theme": { + "auto": { + "label": "" + }, + "dark": { + "label": "" + }, + "description": "", + "light": { + "label": "" + }, + "title": "" + }, + "title": "Användarinställningar" + }, + "users": { + "add_user": { + "submit": "Lägg till användare" + }, + "confirm_delete_user": { + "action": "Radera {{user}}", + "description": "<0>Är du säker att du vill radera <1>?

    Denna åtgärd kan inte ångras

    ", + "title": "Radera användare" + }, + "password_reset": { + "description": "Ändra lösenord för <1>{{username}}", + "form": { + "label": "Nytt lösenord", + "placeholder": "lösenord", + "submit": "Byta lösenord" + }, + "title": "Byta lösenord" + }, + "table": { + "column_names": { + "action": "Åtgärd", + "capabilities": "", + "photo_path": "Sökväg till bild", + "username": "Användarnamn" + }, + "new_user": "Ny användare", + "row": { + "action": { + "change_password": "Byta lösenord", + "delete": "Radera", + "edit": "Redigera", + "scan": "Skanna" + } + } + }, + "title": "Användare" + }, + "version_info": { + "build_date_title": "", + "title": "", + "version_title": "" + } + }, + "share_page": { + "media": { + "title": "Delad media" + }, + "protected_share": { + "description": "Denna delning är skyddad med ett lösenord.", + "password_required_error": "", + "title": "Skyddad delning" + }, + "share_not_found": "Delning hittades inte", + "share_not_found_description": "Delningen har kanske gått ut eller tagits bort.", + "wrong_password": "Fel lösenord, försök igen." + }, + "sidebar": { + "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", + "title_placeholder": "" + }, + "download": { + "filesize": { + "byte_one": "", + "byte_other": "", + "giga_byte_one": "", + "giga_byte_other": "", + "kilo_byte_one": "", + "kilo_byte_other": "", + "mega_byte_one": "", + "mega_byte_other": "", + "tera_byte_one": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "Mått", + "file_size": "Storlek", + "file_type": "Typ", + "name": "Namn" + }, + "title": "Ladda ner" + }, + "location": { + "title": "" + }, + "media": { + "album_path": "", + "exif": { + "description": "", + "exposure_program": { + "action_program": "Actionprogram", + "aperture_priority": "Bländarprioritet", + "bulb": "Bulb", + "creative_program": "Kreativ program", + "landscape_mode": "Landskapsläge", + "manual": "Manuell", + "normal_program": "Normal program", + "not_defined": "Odefinierad", + "portrait_mode": "Porträttläge", + "shutter_priority": "Slutarprioritet" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Avfyrades ej", + "fired": "Avfyrades", + "no_flash": "Ingen blixt", + "no_flash_function": "Ingen blixt funktion", + "off": "Av", + "on": "På", + "red_eye_reduction": "Minskning av röda ögon", + "return_detected": "Retur upttäcktes", + "return_not_detected": "Retur upttäcktes inte" + }, + "name": { + "aperture": "Bländare", + "camera": "Kamera", + "coordinates": "", + "date_shot": "Datum tagen", + "exposure": "Exponering", + "exposure_program": "Program", + "flash": "Blixt", + "focal_length": "Brännvidd", + "iso": "ISO", + "lens": "Objektiv", + "maker": "Märke" + } + } + }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "confirm_image_detach": "", + "title": "" + }, + "sharing": { + "add_share": "Dela", + "copy_link": "Kopiera länk", + "delete": "", + "more": "", + "no_shares_found": "Inga delningar hittades", + "public_link": "Publika länkar", + "title": "Delningsinställningar" + } + }, + "sidemenu": { + "albums": "Album", + "people": "Personer", + "photos": "Bilder", + "places": "Platser", + "settings": "Inställningar" + }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, + "title": { + "loading_album": "Laddar album", + "login": "", + "people": "Personer", + "settings": "Inställningar" + } +} diff --git a/ui/src/extractedTranslations/sv/translation_old.json b/ui/src/extractedTranslations/sv/translation_old.json new file mode 100644 index 0000000..0bf4703 --- /dev/null +++ b/ui/src/extractedTranslations/sv/translation_old.json @@ -0,0 +1,176 @@ +{ + "album_filter": { + "sort": null + }, + "albums_page": { + "title": "Album" + }, + "general": { + "action": { + "delete": "Radera", + "more": null + }, + "loading": { + "media": "Laddar media", + "timeline": "Laddar tidsaxel" + } + }, + "header": { + "search": { + "result_type": { + "media": null, + "photos": "Bilder" + } + } + }, + "people_page": { + "action_label": { + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, + "unlabeled_person": null + }, + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": "", + "destination_table": { + "title": null + }, + "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + }, + "tableselect_face_group": { + "search_faces_placeholder": null + }, + "tableselect_image_faces": { + "search_images_placeholder": null + } + }, + "places_page": { + "title": null + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + }, + "users": { + "table": { + "column_names": { + "admin": "Admin", + "capabilities": null + } + } + }, + "version_info": { + "build_date_title": null, + "title": null, + "version_title": null + } + }, + "share_page": { + "protected_share": { + "password_required_error": null + } + }, + "sidebar": { + "album": { + "album_cover": null, + "cover_photo": "", + "reset_cover": null, + "set_cover": null, + "title": "Albuminställningar", + "title_placeholder": null + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "" + } + }, + "media": { + "album": "" + }, + "sharing": { + "delete": null, + "more": null, + "table_header": "Publika delningar" + } + }, + "timeline_filter": { + "date": { + "dropdown_all": null, + "label": null + } + }, + "title": { + "login": null + } +} diff --git a/ui/src/extractedTranslations/tr/translation.json b/ui/src/extractedTranslations/tr/translation.json new file mode 100644 index 0000000..76b5af5 --- /dev/null +++ b/ui/src/extractedTranslations/tr/translation.json @@ -0,0 +1,364 @@ +{ + "album_filter": { + "only_favorites": "Sadece favorileri göster", + "sort": "Sırala", + "sort_by": "Sırala:", + "sort_direction": "Sıralama yönü", + "order_direction": { + "ascending": "Artan", + "descending": "Azalan" + }, + "sorting_options": { + "date_imported": "Alındığı tarih", + "date_shot": "Çekildiği tarih", + "title": "Başlık", + "type": "Tür" + } + }, + "general": { + "action": { + "add": "Ekle", + "cancel": "İptal", + "remove": "Kaldır", + "save": "Kaydet" + }, + "loading": { + "album": "Albüm yükleniyor", + "default": "Yükleniyor...", + "page": "Sayfa yükleniyor", + "paginate": { + "faces": "Daha fazla kişi yükleniyor", + "media": "Daha fazla medya yükleniyor" + }, + "shares": "Paylaşımlar yükleniyor..." + } + }, + "header": { + "search": { + "loading": "Sonuçlar yükleniyor...", + "no_results": "Sonuç bulunamadı", + "placeholder": "Ara", + "result_type": { + "albums": "Albümler", + "media": "Medya" + } + } + }, + "login_page": { + "field": { + "password": "Parola", + "submit": "Oturum aç", + "username": "Kullanıcı adı" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Fotoğraf yolu", + "placeholder": "/path/to/photos" + }, + "submit": "Photoview Kurulumu" + }, + "title": "İlk Kurulum" + }, + "welcome": "Photoview Uygulamasına Hoş Geldiniz" + }, + "meta": { + "description": "Kişisel Sunucular için Basit ve Kullanıcı Dostu Fotoğraf Galerisi" + }, + "people_page": { + "action_label": { + "change_label": "Etiketi değiştir", + "detach_images": "Yüzü ayır", + "merge_people": "Yüzü birleştir", + "move_faces": "Yüzleri taşı" + }, + "face_group": { + "label_placeholder": "Etiket", + "unlabeled": "Etiketsiz", + "unlabeled_person": "Etiketsiz kişi" + }, + "modal": { + "action": { + "merge": "Birleştir" + }, + "detach_image_faces": { + "action": { + "detach": "Resimdeki yüzleri ayır", + "select_images": "Ayırılacak resimleri seçin" + }, + "description": "Bu yüz grubunun seçili resimlerini ayırın ve bunları yeni bir yüz grubuna taşıyın", + "title": "Resimdeki Yüzleri Ayır" + }, + "merge_face_groups": { + "description": "Bu yüz grubundaki tüm resimler seçilen yüz grubunda birleştirilecektir.", + "destination_table": { + "title": "Hedef yüzü seçin" + }, + "title": "Yüz Gruplarını Birleştir" + }, + "move_image_faces": { + "description": "Bu yüz grubunun seçili resimlerini başka bir yüz grubuna taşı", + "destination_face_group_table": { + "move_action": "Resimdeki yüzleri taşı", + "title": "Hedef yüz grubunu seçin" + }, + "image_select_table": { + "next_action": "Sonraki", + "title": "Taşınacak resimleri seçin" + }, + "title": "Resimdeki Yüzleri Taşı" + } + }, + "recognize_unlabeled_faces_button": "Etiketlenmemiş yüzleri tanı", + "tableselect_face_group": { + "search_faces_placeholder": "Yüzlerde ara..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Resimlerde ara..." + } + }, + "photos_page": { + "title": "Zaman çizelgesi" + }, + "places_page": { + "title": "Yerler" + }, + "routes": { + "page_not_found": "Sayfa bulunamadı" + }, + "settings": { + "concurrent_workers": { + "description": "Aynı anda çalışmasına izin verilen tarayıcı işi miktarı", + "title": "Eş zamanlı tarayıcı işlemleri" + }, + "logout": "Oturumu kapat", + "periodic_scanner": { + "checkbox_label": "Düzenli tarayıcıyı etkinleştir", + "field": { + "description": "Tarayıcının tüm kullanıcılar için otomatik taramaları ne sıklıkla gerçekleştirmesi gerekir", + "label": "Düzenli tarama aralığı" + }, + "interval_unit": { + "days": "Gün", + "hour": "Saat", + "minutes": "Dakika", + "months": "Ay", + "seconds": "Saniye" + }, + "title": "Düzenli tarayıcı" + }, + "scanner": { + "description": "Yeni veya güncellenmiş medya için tüm kullanıcıları tarar", + "scan_all_users": "Tüm kullanıcıları tara", + "title": "Tarayıcı" + }, + "user_preferences": { + "change_language": { + "description": "Bu kullanıcıya özel web sitesi dilini değiştir", + "label": "Web sitesi dili" + }, + "language_selector": { + "placeholder": "Dil seç" + }, + "theme": { + "auto": { + "label": "Sistem ile aynı" + }, + "dark": { + "label": "Koyu" + }, + "description": "Web sitesinin görünümünü değiştirin", + "light": { + "label": "Açık" + }, + "title": "Tema tercihleri" + }, + "title": "Kullanıcı tercihleri" + }, + "users": { + "add_user": { + "submit": "Kullanıcı ekle" + }, + "confirm_delete_user": { + "action": "Sil: {{user}}", + "description": "<0>Silmek istediğinize emin misiniz <1>?

    Bu işlem geri alınamaz

    ", + "title": "Kullanıcı sil" + }, + "password_reset": { + "description": "<1>{{username}} için parola değiştir", + "form": { + "label": "Yeni parola", + "placeholder": "parola", + "submit": "Parolayı değiştir" + }, + "title": "Parola değiştir" + }, + "table": { + "column_names": { + "action": "Eylem", + "capabilities": "Özellikler", + "photo_path": "Fotoğraf yolu", + "username": "Kullanıcı adı" + }, + "new_user": "Yeni kullanıcı", + "row": { + "action": { + "change_password": "Parola değiştir", + "delete": "Sil", + "edit": "Düzenle", + "scan": "Tara" + } + } + }, + "title": "Kullanıcılar" + }, + "version_info": { + "build_date_title": "Yapı tarihi", + "title": "Photoview Sürümü", + "version_title": "Yayın Sürümü" + } + }, + "share_page": { + "media": { + "title": "Paylaşılan medya" + }, + "protected_share": { + "description": "Bu paylaşım şifre ile korunmaktadır.", + "password_required_error": "Parola gereklidir", + "title": "Korumalı paylaşım" + }, + "share_not_found": "Paylaşım bulunamadı", + "share_not_found_description": "Paylaşımın süresi dolmuş veya silinmiş olabilir.", + "wrong_password": "Yanlış parola, lütfen tekrar deneyin." + }, + "sidebar": { + "album": { + "album_cover": "Albüm kapağı", + "download": { + "high-resolutions": { + "description": "RAW resimlerin yüksek çözünürlüklü jpeg dosyaları", + "title": "Yüksek çözünürlükler" + }, + "originals": { + "description": "Orijinal resimler ve videolar", + "title": "Orijinaller" + }, + "thumbnails": { + "description": "Düşük çözünürlüklü resimler, video yok", + "title": "Küçük resimler" + }, + "web-videos": { + "description": "Web için optimize edilmiş videolar", + "title": "Dönüştürülmüş videolar" + } + }, + "reset_cover": "Kapak fotoğrafını sıfırla", + "set_cover": "Albüm kapak fotoğrafı olarak ayarla", + "title_placeholder": "Albüm başlığı" + }, + "download": { + "filesize": { + "byte_one": "{{count}} Bayt", + "byte_other": "{{count}} Bayt", + "giga_byte_one": "{{count}} GB", + "giga_byte_other": "{{count}} GB", + "kilo_byte_one": "{{count}} KB", + "kilo_byte_other": "{{count}} KB", + "mega_byte_one": "{{count}} MB", + "mega_byte_other": "{{count}} MB", + "tera_byte_one": "{{count}} TB", + "tera_byte_other": "{{count}} TB" + }, + "table_columns": { + "dimensions": "Boyutlar", + "file_size": "Boyut", + "file_type": "Tür", + "name": "Ad" + }, + "title": "İndir" + }, + "location": { + "title": "Konum" + }, + "media": { + "album_path": "Albüm yolu", + "exif": { + "exposure_program": { + "action_program": "Action program", + "aperture_priority": "Diyafram önceliği", + "bulb": "Ampul", + "creative_program": "Yaratıcı izlence", + "landscape_mode": "Manzara modu", + "manual": "El ile", + "normal_program": "Normal izlence", + "not_defined": "Tanımlanmamış", + "portrait_mode": "Portre modu", + "shutter_priority": "Perde önceliği" + }, + "flash": { + "auto": "Otomatik", + "did_not_fire": "Patlamadı", + "fired": "Patladı", + "no_flash": "Flaşsız", + "no_flash_function": "Flaş fonksiyonu yok", + "off": "Kapalı", + "on": "Açık", + "red_eye_reduction": "Kırmızı göz önleme", + "return_detected": "Dönüş algılandı", + "return_not_detected": "Dönüş algılanmadı" + }, + "name": { + "aperture": "Diyafram", + "camera": "Kamera", + "coordinates": "Koordinatlar", + "date_shot": "Çekildiği tarih", + "exposure": "Pozlandırma", + "exposure_program": "Program", + "flash": "Flaş", + "focal_length": "Odak uzunluğu", + "iso": "ISO", + "lens": "Lens", + "maker": "Üretici" + } + } + }, + "people": { + "action_label": { + "detach_image": "Resmi ayır", + "merge_face": "Yüzü birleştir", + "move_face": "Yüzü taşı" + }, + "confirm_image_detach": "Bu resmi ayırmak istediğinizden emin misiniz?", + "title": "Kişiler" + }, + "sharing": { + "add_share": "Paylaşımlara ekle", + "copy_link": "Bağlantıyı Kopyala", + "delete": "Sil", + "more": "Diğer", + "no_shares_found": "Paylaşım bulunamadı", + "public_link": "Genel bağlantı", + "title": "Paylaşım seçenekleri" + } + }, + "sidemenu": { + "albums": "Albümler", + "people": "Kişiler", + "photos": "Zaman Çizelgesi", + "places": "Yerler", + "settings": "Ayarlar" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Bugün", + "dropdown_year": "{{year}} ve öncesi", + "label": "Tarih" + } + }, + "title": { + "loading_album": "Albüm yükleniyor", + "login": "Oturum Aç", + "people": "Kişiler", + "settings": "Ayarlar" + } +} diff --git a/ui/src/extractedTranslations/uk/translation.json b/ui/src/extractedTranslations/uk/translation.json new file mode 100644 index 0000000..b42daa9 --- /dev/null +++ b/ui/src/extractedTranslations/uk/translation.json @@ -0,0 +1,364 @@ +{ + "album_filter": { + "only_favorites": "Показати тільки улюблені", + "sort": "Сортування", + "sort_by": "Сортувати за", + "sort_direction": "Напрям сортування", + "order_direction": { + "ascending": "За зростанням", + "descending": "За спаданням" + }, + "sorting_options": { + "date_imported": "Дата імпортування", + "date_shot": "Дата фотографування", + "title": "Назва", + "type": "Тип" + } + }, + "general": { + "action": { + "add": "Додати", + "cancel": "Відмінити", + "remove": "Видалити", + "save": "Зберегти" + }, + "loading": { + "album": "Завантаження альбому", + "default": "Завантаження...", + "page": "Завантаження сторінки", + "paginate": { + "faces": "Завантаження ще людей", + "media": "Завантаження ще медіафайлів" + }, + "shares": "Завантаження спільних ресурсів..." + } + }, + "header": { + "search": { + "loading": "Завантаження результатів...", + "no_results": "Результатів не знайдено", + "placeholder": "Пошук", + "result_type": { + "albums": "Альбоми", + "media": "Медіафайли" + } + } + }, + "login_page": { + "field": { + "password": "Пароль", + "submit": "Увійти", + "username": "Імʼя користувача" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "Шлях до медіафайлів", + "placeholder": "/шлях/до/медіафайлів" + }, + "submit": "Налаштувати Photoview" + }, + "title": "Початкове налаштування" + }, + "welcome": "Ласкаво просимо до Photoview" + }, + "meta": { + "description": "Проста та зручна фотогалерея для персональних серверів" + }, + "people_page": { + "action_label": { + "change_label": "Змінити мітку", + "detach_images": "Від’єднати обличчя", + "merge_people": "Обʼєднати обличчя", + "move_faces": "Перемістити обличчя" + }, + "face_group": { + "label_placeholder": "Мітка", + "unlabeled": "Без мітки", + "unlabeled_person": "Людина без мітки" + }, + "modal": { + "action": { + "merge": "Обʼєднати" + }, + "detach_image_faces": { + "action": { + "detach": "Від’єднати світлини з обличчями", + "select_images": "Вибрати світлини для від’єднання" + }, + "description": "Від’єднати обрані світлини цієї групи обличь та перемістити їх до нових груп", + "title": "Від’єднати світлини з обличчями" + }, + "merge_face_groups": { + "description": "Усі світлини цієї групи обличь будуть обʼєднані з обраною групою.", + "destination_table": { + "title": "Оберіть обличчя призначення" + }, + "title": "Об’єднати групи обличь" + }, + "move_image_faces": { + "description": "Перемістити вибрані світлини цієї групи обличь до іншої групи обличь", + "destination_face_group_table": { + "move_action": "Перемістити світлини обличь", + "title": "Оберіть групу обличь призначення" + }, + "image_select_table": { + "next_action": "Далі", + "title": "Оберіть світлини для переміщення" + }, + "title": "Перемістити світлини обличь" + } + }, + "recognize_unlabeled_faces_button": "Розпізнати обличчя без міток", + "tableselect_face_group": { + "search_faces_placeholder": "Шукати обличчя..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "Шукати світлини..." + } + }, + "photos_page": { + "title": "Хронологія" + }, + "places_page": { + "title": "Місця" + }, + "routes": { + "page_not_found": "Сторінку не знайдено" + }, + "settings": { + "concurrent_workers": { + "description": "Максимальна кількість завдань сканування, які можна запускати одночасно", + "title": "Кількість одночасних оброблювачів завдань" + }, + "logout": "Завершити сесію користувача", + "periodic_scanner": { + "checkbox_label": "Увімкнути періодичне сканування", + "field": { + "description": "Як часто сканер повинен виконувати автоматичне сканування для всіх користувачів", + "label": "Періодичний інтервал сканування" + }, + "interval_unit": { + "days": "Дні", + "hour": "Години", + "minutes": "Хвилини", + "months": "Місяці", + "seconds": "Секунди" + }, + "title": "Періодичне сканування" + }, + "scanner": { + "description": "Сканує всіх користувачів на наявність нових або оновлених медіафайлів", + "scan_all_users": "Сканувати всіх користувачів", + "title": "Сканер" + }, + "user_preferences": { + "change_language": { + "description": "Змінити мову сайту для цього користувача", + "label": "Мова сайту" + }, + "language_selector": { + "placeholder": "Обрати мову" + }, + "theme": { + "auto": { + "label": "Така-ж, як в системі" + }, + "dark": { + "label": "Темна" + }, + "description": "Змінити зовнішній вигляд сайту", + "light": { + "label": "Світла" + }, + "title": "Налаштування теми" + }, + "title": "Налаштування користувача" + }, + "users": { + "add_user": { + "submit": "Додати користувача" + }, + "confirm_delete_user": { + "action": "Видалити {{user}}", + "description": "<0>Ви впевнені, що бажаєте видалити <1>?

    Цю дію не можна буде скасувати

    ", + "title": "Видалити користувача" + }, + "password_reset": { + "description": "Зміна пароля для <1>{{username}}", + "form": { + "label": "Новий пароль", + "placeholder": "пароль", + "submit": "Змінити пароль" + }, + "title": "Зміна пароля" + }, + "table": { + "column_names": { + "action": "Дія", + "capabilities": "Можливості", + "photo_path": "Шлях до медіафайлів", + "username": "Імʼя користувача" + }, + "new_user": "Новий користувач", + "row": { + "action": { + "change_password": "Змінити пароль", + "delete": "Видалити", + "edit": "Редагувати", + "scan": "Сканувати" + } + } + }, + "title": "Користувачі" + }, + "version_info": { + "build_date_title": "Дата збірки", + "title": "Версія Photoview", + "version_title": "Версія релізу" + } + }, + "share_page": { + "media": { + "title": "Спільні медіатеки" + }, + "protected_share": { + "description": "Ця спільна медіатека захищена паролем.", + "password_required_error": "Пароль потрібен", + "title": "Захищена медіатека" + }, + "share_not_found": "Спільну медіатеку не знайдено", + "share_not_found_description": "Можливо, спільний доступ закінчився або його було видалено.", + "wrong_password": "Неправильний пароль, спробуйте ще раз." + }, + "sidebar": { + "album": { + "album_cover": "Обкладинка альбому", + "download": { + "high-resolutions": { + "description": "JPEG-файли високої роздільної здатності RAW-зображень", + "title": "Висока роздільна здатність" + }, + "originals": { + "description": "Оригінальні світлини та відео", + "title": "Оригінали" + }, + "thumbnails": { + "description": "Зображення з низькою роздільною здатністю, без відео", + "title": "Мініатюри" + }, + "web-videos": { + "description": "Відео, оптимізовані для Інтернету", + "title": "Конвертовані відео" + } + }, + "reset_cover": "Скинути обкладинку", + "set_cover": "Встановити як обкладинку альбому", + "title_placeholder": "Назва альбому" + }, + "download": { + "filesize": { + "byte_one": "{{count}} Байт", + "byte_other": "{{count}} Байтів", + "giga_byte_one": "{{count}} ГБ", + "giga_byte_other": "{{count}} ГБ", + "kilo_byte_one": "{{count}} КБ", + "kilo_byte_other": "{{count}} КБ", + "mega_byte_one": "{{count}} МБ", + "mega_byte_other": "{{count}} МБ", + "tera_byte_one": "{{count}} ТБ", + "tera_byte_other": "{{count}} ТБ" + }, + "table_columns": { + "dimensions": "Габарити", + "file_size": "Розмір", + "file_type": "Тип", + "name": "Імʼя" + }, + "title": "Завантажити" + }, + "location": { + "title": "Місцезнаходження" + }, + "media": { + "album_path": "Шлях до альбому", + "exif": { + "exposure_program": { + "action_program": "Програма дії", + "aperture_priority": "Пріоритет діафрагми", + "bulb": "Лампочка", + "creative_program": "Творча програма", + "landscape_mode": "Альбомний режим", + "manual": "Ручний", + "normal_program": "Нормальна програма", + "not_defined": "Не визначений", + "portrait_mode": "Портретний режим", + "shutter_priority": "Пріоритет витримки" + }, + "flash": { + "auto": "Автоматичний", + "did_not_fire": "Не стріляв", + "fired": "Стріляв", + "no_flash": "Без спалаху", + "no_flash_function": "Немає функції спалаху", + "off": "Вимкнено", + "on": "Увімкнено", + "red_eye_reduction": "Зменшення ефекту червоних очей", + "return_detected": "Повернення виявлено", + "return_not_detected": "Повернення не виявлено" + }, + "name": { + "aperture": "Діафрагма", + "camera": "Камера", + "coordinates": "Координати", + "date_shot": "Дата зйомки", + "exposure": "Експозиція", + "exposure_program": "Експозиційна програма", + "flash": "Спалах", + "focal_length": "Фокусна відстань", + "iso": "ISO", + "lens": "Об'єктив", + "maker": "Виробник" + } + } + }, + "people": { + "action_label": { + "detach_image": "Від’єднати зображення", + "merge_face": "Об'єднати обличчя", + "move_face": "Перемістити обличчя" + }, + "confirm_image_detach": "Ви впевнені, що бажаєте від’єднати це зображення?", + "title": "Люди" + }, + "sharing": { + "add_share": "Додати спільний доступ", + "copy_link": "Копіювати посилання", + "delete": "Видалити", + "more": "Більше", + "no_shares_found": "Спільних доступів не знайдено", + "public_link": "Публічне посилання", + "title": "Параметри спільного доступу" + } + }, + "sidemenu": { + "albums": "Альбоми", + "people": "Люди", + "photos": "Хронологія", + "places": "Місця", + "settings": "Налаштування" + }, + "timeline_filter": { + "date": { + "dropdown_all": "Від сьогодні", + "dropdown_year": "{{year}} та раніше", + "label": "Дата" + } + }, + "title": { + "loading_album": "Завантаження альбому", + "login": "Логін", + "people": "Люди", + "settings": "Налаштування" + } +} diff --git a/ui/src/extractedTranslations/zh-CN/translation.json b/ui/src/extractedTranslations/zh-CN/translation.json new file mode 100644 index 0000000..ca7dd11 --- /dev/null +++ b/ui/src/extractedTranslations/zh-CN/translation.json @@ -0,0 +1,367 @@ +{ + "album_filter": { + "only_favorites": "只显示喜爱", + "sort": "排序", + "sort_by": "排序方式", + "sort_direction": "排序方向", + "order_direction": { + "ascending": "升序", + "descending": "降序" + }, + "sorting_options": { + "date_imported": "加入日期", + "date_shot": "拍摄日期", + "title": "标题", + "type": "文件类型" + } + }, + "general": { + "action": { + "add": "加入", + "cancel": "取消", + "remove": "删除", + "save": "储存" + }, + "loading": { + "album": "载入相册中", + "default": "载入中...", + "page": "载入页面中", + "paginate": { + "faces": "载入更多面孔", + "media": "载入更多媒体" + }, + "shares": "载入分享..." + } + }, + "header": { + "search": { + "loading": "载入结果...", + "no_results": "找不到结果", + "placeholder": "查找", + "result_type": { + "albums": "相册", + "media": "媒体" + } + } + }, + "login_page": { + "field": { + "password": "密码", + "submit": "登入", + "username": "用户名" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "相册路径", + "placeholder": "/path/to/photos" + }, + "submit": "设置 Photoview" + }, + "title": "初始设置" + }, + "welcome": "欢迎使用 Photoview" + }, + "meta": { + "description": "为个人服务器打造的简单及易用的照片库" + }, + "people_page": { + "action_label": { + "change_label": "修改面孔命名", + "detach_images": "解除面孔标记", + "merge_face": "", + "merge_people": "合并面孔至", + "move_faces": "移动面孔至" + }, + "face_group": { + "label_placeholder": "名称", + "unlabeled": "未命名", + "unlabeled_person": "未命名人物" + }, + "modal": { + "action": { + "merge": "合并", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "解除标记", + "select_images": "选择要解除标记的照片" + }, + "description": "将该组面孔选中的照片取消标记,并移动至新的面孔", + "title": "解除面孔标记" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "选择目标面孔" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "合并面孔组" + }, + "move_image_faces": { + "description": "将该组面孔中选择的照片移动到另一组面孔", + "destination_face_group_table": { + "move_action": "移动照片", + "title": "选择目标面孔" + }, + "image_select_table": { + "next_action": "下一步", + "title": "选择想移动的照片" + }, + "title": "移动照片面孔" + } + }, + "recognize_unlabeled_faces_button": "已识别的未命名面孔", + "tableselect_face_group": { + "search_faces_placeholder": "搜寻面孔..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "搜寻照片..." + } + }, + "photos_page": { + "title": "时间线" + }, + "places_page": { + "title": "地点" + }, + "routes": { + "page_not_found": "找不到网页" + }, + "settings": { + "concurrent_workers": { + "description": "最多允许同时进行的扫描任务数目", + "title": "同时扫描任务" + }, + "logout": "登出", + "periodic_scanner": { + "checkbox_label": "启用周期性扫描", + "field": { + "description": "对全部用户自动扫描的频率", + "label": "扫描频率" + }, + "interval_unit": { + "days": "天", + "hour": "小时", + "minutes": "分钟", + "months": "月", + "seconds": "秒" + }, + "title": "周期性扫描" + }, + "scanner": { + "description": "将扫描所有用户新添加及更新的媒体文件", + "scan_all_users": "扫描所有用户", + "title": "扫描" + }, + "user_preferences": { + "change_language": { + "description": "修改网站语言,仅对本用户生效", + "label": "网站语言" + }, + "language_selector": { + "placeholder": "选择语言" + }, + "theme": { + "auto": { + "label": "跟随系统" + }, + "dark": { + "label": "暗黑" + }, + "description": "修改网站外观", + "light": { + "label": "明亮" + }, + "title": "主题设置" + }, + "title": "用户选项" + }, + "users": { + "add_user": { + "submit": "新增用户" + }, + "confirm_delete_user": { + "action": "删除 {{user}}", + "description": "<0>确定删除 <1>?

    此操作无法取消

    ", + "title": "删除用户" + }, + "password_reset": { + "description": "更改 <1>{{username}} 的密码", + "form": { + "label": "新密码", + "placeholder": "密码", + "submit": "提交" + }, + "title": "更改密码" + }, + "table": { + "column_names": { + "action": "操作", + "capabilities": "权限", + "photo_path": "相册路径", + "username": "用户名" + }, + "new_user": "新增用户", + "row": { + "action": { + "change_password": "更改密码", + "delete": "删除", + "edit": "更改", + "scan": "扫描" + } + } + }, + "title": "用户" + }, + "version_info": { + "build_date_title": "编译日期", + "title": "Photoview 版本", + "version_title": "发行版" + } + }, + "share_page": { + "media": { + "title": "已分享媒体" + }, + "protected_share": { + "description": "此分享受密码保护", + "password_required_error": "需要密码", + "title": "已保护的分享" + }, + "share_not_found": "找不到分享", + "share_not_found_description": "可能此分享已过期或已删除", + "wrong_password": "密码错误,请重试" + }, + "sidebar": { + "album": { + "album_cover": "相册封面", + "download": { + "high-resolutions": { + "description": "原始图片的高清jpeg版本", + "title": "高清" + }, + "originals": { + "description": "原始的图片和视频", + "title": "原文件" + }, + "thumbnails": { + "description": "低清晰度的图片,不包含视频", + "title": "低清" + }, + "web-videos": { + "description": "为了网页展示而优化过的视频", + "title": "已转换视频" + } + }, + "reset_cover": "重设封面照片", + "set_cover": "设置为封面图片", + "title_placeholder": "相册名称" + }, + "download": { + "filesize": { + "byte_one": "{{count}} 字节", + "byte_other": "{{count}} 字节", + "giga_byte_other": "", + "kilo_byte_other": "", + "mega_byte_other": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "尺寸", + "file_size": "大小", + "file_type": "格式", + "name": "名称" + }, + "title": "下载" + }, + "location": { + "title": "位置" + }, + "media": { + "album_path": "", + "exif": { + "description": "", + "exposure_program": { + "action_program": "动态模式", + "aperture_priority": "光圈优先", + "bulb": "曝光模式", + "creative_program": "创意模式", + "landscape_mode": "风景模式", + "manual": "手动模式", + "normal_program": "普通模式", + "not_defined": "未定义", + "portrait_mode": "肖像模式", + "shutter_priority": "快门优先" + }, + "flash": { + "auto": "自动", + "did_not_fire": "未使用闪光灯", + "fired": "使用闪光灯", + "no_flash": "无闪光灯", + "no_flash_function": "无闪光灯功能", + "off": "关闭", + "on": "使用", + "red_eye_reduction": "减轻红眼", + "return_detected": "检测到回光", + "return_not_detected": "未检测到回光" + }, + "name": { + "aperture": "光圈", + "camera": "相机", + "coordinates": "坐标", + "date_shot": "拍摄日期", + "exposure": "曝光", + "exposure_program": "模式", + "flash": "闪光灯", + "focal_length": "焦距", + "iso": "ISO", + "lens": "镜头", + "maker": "制造商" + } + } + }, + "people": { + "action_label": { + "detach_image": "解除面孔标记", + "merge_face": "合并面孔", + "move_face": "移动面孔" + }, + "confirm_image_detach": "您确认要取消该图片的面孔标记吗?", + "title": "人物" + }, + "sharing": { + "add_share": "新增分享", + "copy_link": "复制链接", + "delete": "删除分享", + "more": "更多", + "no_shares_found": "没有分享", + "public_link": "公开链接", + "title": "分享选项" + } + }, + "sidemenu": { + "albums": "相册", + "people": "人物", + "photos": "时间线", + "places": "地点", + "settings": "设置" + }, + "timeline_filter": { + "date": { + "dropdown_all": "最新", + "dropdown_year": "{{year}}及更早", + "label": "日期" + } + }, + "title": { + "loading_album": "载入相册", + "login": "登入", + "people": "人物", + "settings": "设置" + } +} diff --git a/ui/src/extractedTranslations/zh-CN/translation_old.json b/ui/src/extractedTranslations/zh-CN/translation_old.json new file mode 100644 index 0000000..9a704fd --- /dev/null +++ b/ui/src/extractedTranslations/zh-CN/translation_old.json @@ -0,0 +1,62 @@ +{ + "people_page": { + "action_label": { + "change_label": null, + "detach_images": null, + "merge_people": null, + "move_faces": null + }, + "modal": { + "merge_face_groups": { + "description": "该组面孔中的所有照片都会被合并至选择的面孔" + }, + "merge_people_groups": { + "description": "所有在此的脸孔群组将会合并到已选择的脸孔群组", + "destination_table": { + "title": "选择目标脸孔群组" + }, + "title": "合并脸孔群组" + } + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + } + }, + "sidebar": { + "download": { + "filesize": { + "byte": "", + "giga_byte": "", + "kilo_byte": "", + "mega_byte": "", + "tera_byte": "" + } + }, + "media": { + "exif": { + "flash": { + "did_not_fire": null, + "fired": null, + "no_flash": null, + "no_flash_function": null, + "return_detected": null, + "return_not_detected": null + } + } + } + } +} diff --git a/ui/src/extractedTranslations/zh-HK/translation.json b/ui/src/extractedTranslations/zh-HK/translation.json new file mode 100644 index 0000000..bef35bb --- /dev/null +++ b/ui/src/extractedTranslations/zh-HK/translation.json @@ -0,0 +1,366 @@ +{ + "album_filter": { + "only_favorites": "只顯示喜愛", + "sort": "排序", + "sort_by": "排序方式", + "sort_direction": "排序方向", + "order_direction": { + "ascending": "升序", + "descending": "降序" + }, + "sorting_options": { + "date_imported": "加入日期", + "date_shot": "拍攝日期", + "title": "標題", + "type": "種類" + } + }, + "general": { + "action": { + "add": "加入", + "cancel": "取消", + "remove": "刪除", + "save": "儲存" + }, + "loading": { + "album": "載入相簿中", + "default": "載入中...", + "page": "載入頁面中", + "paginate": { + "faces": "載入更多臉孔", + "media": "載入更多媒體" + }, + "shares": "載入分享..." + } + }, + "header": { + "search": { + "loading": "載入結果...", + "no_results": "找不到結果", + "placeholder": "搜尋", + "result_type": { + "albums": "相簿", + "media": "媒體" + } + } + }, + "login_page": { + "field": { + "password": "密碼", + "submit": "登入", + "username": "使用者名稱" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "相簿路徑", + "placeholder": "/path/to/photos" + }, + "submit": "設定 Photoview" + }, + "title": "初始設定" + }, + "welcome": "歡迎使用 Photoview" + }, + "meta": { + "description": "簡單及易用的照片庫給個人伺服器" + }, + "people_page": { + "action_label": { + "change_label": "", + "detach_images": "", + "merge_face": "", + "merge_people": "", + "move_faces": "" + }, + "face_group": { + "label_placeholder": "名稱", + "unlabeled": "未命名", + "unlabeled_person": "未命名人物" + }, + "modal": { + "action": { + "merge": "合併", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "分離相片臉孔", + "select_images": "選擇分離相片" + }, + "description": "分離已選擇相片中的臉孔群組及移動到新臉孔群組", + "title": "分離相片中的臉孔" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "選擇目標臉孔群組" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "合併臉孔群組" + }, + "move_image_faces": { + "description": "移動已選擇的臉孔群組到另一個面孔群組", + "destination_face_group_table": { + "move_action": "移動相片的面孔", + "title": "選擇目標臉孔群組" + }, + "image_select_table": { + "next_action": "下一步", + "title": "選擇想移動的相片" + }, + "title": "移動相片臉孔" + } + }, + "recognize_unlabeled_faces_button": "已識別的未命名臉孔", + "tableselect_face_group": { + "search_faces_placeholder": "搜尋臉孔..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "搜尋相片..." + } + }, + "photos_page": { + "title": "相片" + }, + "places_page": { + "title": "地點" + }, + "routes": { + "page_not_found": "找不到網頁" + }, + "settings": { + "concurrent_workers": { + "description": "最多同時進行的掃描工作數目", + "title": "同時掃描工作" + }, + "logout": "登出", + "periodic_scanner": { + "checkbox_label": "啟用週期性掃描", + "field": { + "description": "對全部使用者自動掃描的頻率", + "label": "掃描頻率" + }, + "interval_unit": { + "days": "日", + "hour": "小時", + "minutes": "分鐘", + "months": "月", + "seconds": "秒" + }, + "title": "週期性掃描" + }, + "scanner": { + "description": "將掃描所有使用者的新媒體或更新媒體", + "scan_all_users": "掃描所有使用者", + "title": "掃描" + }, + "user_preferences": { + "change_language": { + "description": "Change website language specific for this user", + "label": "Website language" + }, + "language_selector": { + "placeholder": "Select language" + }, + "theme": { + "auto": { + "label": "" + }, + "dark": { + "label": "" + }, + "description": "", + "light": { + "label": "" + }, + "title": "" + }, + "title": "使用者選項" + }, + "users": { + "add_user": { + "submit": "新增使用者" + }, + "confirm_delete_user": { + "action": "刪除 {{user}}", + "description": "<0>確定刪除 <1>?

    此操作無法取消

    ", + "title": "刪除使用者" + }, + "password_reset": { + "description": "更改 <1>{{username}} 的密碼", + "form": { + "label": "新密碼", + "placeholder": "密碼", + "submit": "提交" + }, + "title": "更改密碼" + }, + "table": { + "column_names": { + "action": "動作", + "capabilities": "能力", + "photo_path": "相簿路徑", + "username": "使用者名稱" + }, + "new_user": "新增使用者", + "row": { + "action": { + "change_password": "更改密碼", + "delete": "刪除", + "edit": "更改", + "scan": "掃描" + } + } + }, + "title": "使用者" + }, + "version_info": { + "build_date_title": "編譯日期", + "title": "Photoview 版本", + "version_title": "釋出版本" + } + }, + "share_page": { + "media": { + "title": "已分享媒體" + }, + "protected_share": { + "description": "此分享受密碼保護", + "password_required_error": "需要密碼", + "title": "已保護的分享" + }, + "share_not_found": "找不到分享", + "share_not_found_description": "可能此分享已過期或已刪除", + "wrong_password": "密碼錯誤,請重試" + }, + "sidebar": { + "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", + "title_placeholder": "相簿名稱" + }, + "download": { + "filesize": { + "byte_other": "", + "giga_byte_other": "", + "kilo_byte_other": "", + "mega_byte_other": "", + "tera_byte_other": "" + }, + "table_columns": { + "dimensions": "尺寸", + "file_size": "大小", + "file_type": "類型", + "name": "名稱" + }, + "title": "下載" + }, + "location": { + "title": "" + }, + "media": { + "album_path": "", + "exif": { + "description": "", + "exposure_program": { + "action_program": "動態模式", + "aperture_priority": "光圈優先", + "bulb": "曝光模式", + "creative_program": "創意模式", + "landscape_mode": "風景模式", + "manual": "手動模式", + "normal_program": "普通模式", + "not_defined": "未定義", + "portrait_mode": "肖像模式", + "shutter_priority": "快門優先" + }, + "flash": { + "auto": "自動", + "did_not_fire": "", + "fired": "", + "no_flash": "", + "no_flash_function": "", + "off": "關閉", + "on": "使用", + "red_eye_reduction": "減輕紅眼", + "return_detected": "", + "return_not_detected": "" + }, + "name": { + "aperture": "光圈", + "camera": "相機", + "coordinates": "", + "date_shot": "拍攝日期", + "exposure": "曝光", + "exposure_program": "模式", + "flash": "閃光燈", + "focal_length": "焦距", + "iso": "ISO", + "lens": "鏡頭", + "maker": "製造商" + } + } + }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "confirm_image_detach": "", + "title": "" + }, + "sharing": { + "add_share": "新增分享", + "copy_link": "複製連結", + "delete": "刪除分享", + "more": "更多", + "no_shares_found": "沒有分享", + "public_link": "公開連結", + "title": "分享選項" + } + }, + "sidemenu": { + "albums": "相簿", + "people": "人物", + "photos": "時間線", + "places": "地點", + "settings": "設定" + }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, + "title": { + "loading_album": "載入相簿", + "login": "登入", + "people": "人物", + "settings": "設定" + } +} diff --git a/ui/src/extractedTranslations/zh-HK/translation_old.json b/ui/src/extractedTranslations/zh-HK/translation_old.json new file mode 100644 index 0000000..7a72bda --- /dev/null +++ b/ui/src/extractedTranslations/zh-HK/translation_old.json @@ -0,0 +1,62 @@ +{ + "people_page": { + "action_label": { + "change_label": null, + "detach_images": null, + "merge_people": null, + "move_faces": null + }, + "modal": { + "merge_face_groups": { + "description": "所有在此的臉孔群組將會合併到已選擇的臉孔群組" + }, + "merge_people_groups": { + "description": "所有在此的臉孔群組將會合併到已選擇的臉孔群組", + "destination_table": { + "title": "選擇目標臉孔群組" + }, + "title": "合併臉孔群組" + } + } + }, + "settings": { + "thumbnails": { + "method": { + "description": "", + "filter": { + "box": "", + "catmull_rom": "", + "Lanczos": "", + "linear": "", + "mitchell_netravali": "", + "nearest_neighbor": "" + }, + "label": "" + }, + "title": "" + } + }, + "sidebar": { + "download": { + "filesize": { + "byte": "", + "giga_byte": "", + "kilo_byte": "", + "mega_byte": "", + "tera_byte": "" + } + }, + "media": { + "exif": { + "flash": { + "did_not_fire": null, + "fired": null, + "no_flash": null, + "no_flash_function": null, + "return_detected": null, + "return_not_detected": null + } + } + } + } +} diff --git a/ui/src/extractedTranslations/zh-TW/translation.json b/ui/src/extractedTranslations/zh-TW/translation.json new file mode 100644 index 0000000..cf5cb0d --- /dev/null +++ b/ui/src/extractedTranslations/zh-TW/translation.json @@ -0,0 +1,371 @@ +{ + "album_filter": { + "only_favorites": "僅顯示我的最愛", + "sort": "排序", + "sort_by": "排序方式", + "sort_direction": "排序方向", + "order_direction": { + "ascending": "遞增", + "descending": "遞減" + }, + "sorting_options": { + "date_imported": "匯入日期", + "date_shot": "拍攝日期", + "title": "標題", + "type": "類型" + } + }, + "general": { + "action": { + "add": "新增", + "cancel": "取消", + "remove": "移除", + "save": "儲存" + }, + "loading": { + "album": "正在載入相簿", + "default": "載入中...", + "page": "正在載入頁面", + "paginate": { + "faces": "載入更多人物中", + "media": "載入更多媒體中" + }, + "shares": "載入分享項目中..." + } + }, + "header": { + "search": { + "loading": "正在載入搜尋結果...", + "no_results": "找不到符合的結果", + "placeholder": "搜尋", + "result_type": { + "albums": "相簿", + "media": "媒體" + } + } + }, + "login_page": { + "field": { + "password": "密碼", + "submit": "登入", + "username": "使用者名稱" + }, + "initial_setup": { + "field": { + "photo_path": { + "label": "相片路徑", + "placeholder": "/path/to/photos" + }, + "submit": "設定 Photoview" + }, + "title": "初始設定" + }, + "welcome": "歡迎使用 Photoview" + }, + "meta": { + "description": "適合個人伺服器使用的簡潔易用相簿系統" + }, + "people_page": { + "action_label": { + "change_label": "變更標籤", + "detach_images": "移除人臉", + "merge_face": "", + "merge_people": "合併人臉", + "move_faces": "移動人臉" + }, + "face_group": { + "label_placeholder": "標籤", + "unlabeled": "未標示", + "unlabeled_person": "未標示的人物" + }, + "modal": { + "action": { + "merge": "合併", + "next": "" + }, + "detach_image_faces": { + "action": { + "detach": "移除人臉", + "select_images": "選擇要移除的相片" + }, + "description": "將選取的相片從此人臉群組中移除,並移至新的人臉群組", + "title": "移除相片中的人臉" + }, + "merge_face_groups": { + "destination_description": "", + "destination_table": { + "title": "選擇目標人臉" + }, + "sources_description": "", + "sources_table": { + "title": "" + }, + "title": "合併人臉群組" + }, + "move_image_faces": { + "description": "將此人臉群組中選取的相片移至其他人臉群組", + "destination_face_group_table": { + "move_action": "移動相片中的人臉", + "title": "選擇目標人臉群組" + }, + "image_select_table": { + "next_action": "下一步", + "title": "選擇要移動的相片" + }, + "title": "移動相片中的人臉" + } + }, + "recognize_unlabeled_faces_button": "辨識未標示的人臉", + "tableselect_face_group": { + "search_faces_placeholder": "搜尋人臉..." + }, + "tableselect_image_faces": { + "search_images_placeholder": "搜尋相片..." + } + }, + "photos_page": { + "title": "時間軸" + }, + "places_page": { + "title": "地點" + }, + "routes": { + "page_not_found": "找不到頁面" + }, + "settings": { + "concurrent_workers": { + "description": "允許同時執行的掃描工作數量上限", + "title": "掃描器平行工作數" + }, + "logout": "登出", + "periodic_scanner": { + "checkbox_label": "啟用定期掃描", + "field": { + "description": "掃描器對所有使用者執行自動掃描的頻率", + "label": "定期掃描間隔" + }, + "interval_unit": { + "days": "天", + "hour": "小時", + "minutes": "分鐘", + "months": "個月", + "seconds": "秒" + }, + "title": "定期掃描" + }, + "scanner": { + "description": "將會掃描所有使用者以取得新增或更新的媒體", + "scan_all_users": "掃描所有使用者", + "title": "掃描器" + }, + "user_preferences": { + "change_language": { + "description": "變更此使用者的網站語言", + "label": "網站語言" + }, + "language_selector": { + "placeholder": "選擇語言" + }, + "theme": { + "auto": { + "label": "依系統環境" + }, + "dark": { + "label": "深色" + }, + "description": "變更網站外觀", + "light": { + "label": "淺色" + }, + "title": "佈景主題偏好設定" + }, + "title": "使用者偏好設定" + }, + "users": { + "add_user": { + "submit": "新增使用者" + }, + "confirm_delete_user": { + "action": "刪除 {{user}}", + "description": "<0>您確定要刪除 <1> 嗎?

    此動作無法復原

    ", + "title": "刪除使用者" + }, + "password_reset": { + "description": "變更 <1>{{username}} 的密碼", + "form": { + "label": "新密碼", + "placeholder": "密碼", + "submit": "變更密碼" + }, + "title": "變更密碼" + }, + "table": { + "column_names": { + "action": "動作", + "capabilities": "權限", + "photo_path": "相片路徑", + "username": "使用者名稱" + }, + "new_user": "新增使用者", + "row": { + "action": { + "change_password": "變更密碼", + "delete": "刪除", + "edit": "編輯", + "scan": "掃描" + } + } + }, + "title": "使用者" + }, + "version_info": { + "build_date_title": "建置日期", + "title": "Photoview 版本", + "version_title": "發行版本" + } + }, + "share_page": { + "media": { + "title": "已分享的媒體" + }, + "protected_share": { + "description": "此分享內容已設定密碼保護。", + "password_required_error": "請輸入密碼", + "title": "受保護的分享" + }, + "share_not_found": "找不到這個分享內容", + "share_not_found_description": "此分享內容可能已過期或已被刪除。", + "wrong_password": "密碼錯誤,請重試。" + }, + "sidebar": { + "album": { + "album_cover": "相簿封面", + "download": { + "high-resolutions": { + "description": "RAW 檔的高解析度 JPEG 圖片", + "title": "高解析度" + }, + "originals": { + "description": "原始相片和影片", + "title": "原始檔案" + }, + "thumbnails": { + "description": "低解析度相片,不包含影片", + "title": "縮圖" + }, + "web-videos": { + "description": "已針對網頁最佳化的影片", + "title": "轉檔後的影片" + } + }, + "reset_cover": "重設封面相片", + "set_cover": "設為封面相片", + "title_placeholder": "相簿標題" + }, + "download": { + "filesize": { + "byte_one": "{{count}} 位元組", + "byte_other": "{{count}} 位元組", + "giga_byte_one": "{{count}} GB", + "giga_byte_other": "{{count}} GB", + "kilo_byte_one": "{{count}} KB", + "kilo_byte_other": "{{count}} KB", + "mega_byte_one": "{{count}} MB", + "mega_byte_other": "{{count}} MB", + "tera_byte_one": "{{count}} TB", + "tera_byte_other": "{{count}} TB" + }, + "table_columns": { + "dimensions": "尺寸", + "file_size": "大小", + "file_type": "類型", + "name": "名稱" + }, + "title": "下載" + }, + "location": { + "title": "位置資訊" + }, + "media": { + "album_path": "相簿路徑", + "exif": { + "description": "", + "exposure_program": { + "action_program": "動作模式", + "aperture_priority": "光圈先決", + "bulb": "B 快門", + "creative_program": "創意模式", + "landscape_mode": "風景模式", + "manual": "手動", + "normal_program": "一般模式", + "not_defined": "未定義", + "portrait_mode": "人像模式", + "shutter_priority": "快門先決" + }, + "flash": { + "auto": "自動", + "did_not_fire": "未閃光", + "fired": "已閃光", + "no_flash": "無閃光燈", + "no_flash_function": "無閃光燈功能", + "off": "關閉", + "on": "開啟", + "red_eye_reduction": "降低紅眼", + "return_detected": "偵測到反光", + "return_not_detected": "未偵測到反光" + }, + "name": { + "aperture": "光圈", + "camera": "相機", + "coordinates": "座標", + "date_shot": "拍攝日期", + "exposure": "曝光", + "exposure_program": "模式", + "flash": "閃光燈", + "focal_length": "焦距", + "iso": "ISO", + "lens": "鏡頭", + "maker": "製造商" + } + } + }, + "people": { + "action_label": { + "detach_image": "移除相片", + "merge_face": "合併人臉", + "move_face": "移動人臉" + }, + "confirm_image_detach": "您確定要移除這張相片嗎?", + "title": "人物" + }, + "sharing": { + "add_share": "新增分享", + "copy_link": "複製連結", + "delete": "刪除", + "more": "更多", + "no_shares_found": "找不到任何分享內容", + "public_link": "公開連結", + "title": "分享選項" + } + }, + "sidemenu": { + "albums": "相簿", + "people": "人物", + "photos": "時間軸", + "places": "地點", + "settings": "設定" + }, + "timeline_filter": { + "date": { + "dropdown_all": "從今天起", + "dropdown_year": "{{year}} 年以及更早", + "label": "日期" + } + }, + "title": { + "loading_album": "正在載入相簿", + "login": "登入", + "people": "人物", + "settings": "設定" + } +} diff --git a/ui/src/extractedTranslations/zh-TW/translation_old.json b/ui/src/extractedTranslations/zh-TW/translation_old.json new file mode 100644 index 0000000..321765a --- /dev/null +++ b/ui/src/extractedTranslations/zh-TW/translation_old.json @@ -0,0 +1,88 @@ +{ + "albums_page": { + "title": "相簿" + }, + "general": { + "action": { + "delete": "刪除", + "more": "更多" + }, + "loading": { + "media": "正在載入媒體", + "timeline": "正在載入時間軸" + } + }, + "header": { + "search": { + "result_type": { + "photos": "相片" + } + } + }, + "people_page": { + "face_group": { + "action": { + "add_label": "新增標籤", + "change_label": "修改標籤", + "detach_face": "分離人臉", + "merge_face": "合併人臉", + "move_faces": "移動人臉" + } + }, + "modal": { + "merge_face_groups": { + "description": "此人臉群組中的所有相片將會合併至選取的人臉群組。" + }, + "merge_people_groups": { + "description": "此人臉群組中的所有圖片都會合併至所選的人臉群組。", + "destination_table": { + "title": "選擇目標人臉" + }, + "title": "合併人臉群組" + } + }, + "table": { + "select_face_group": { + "search_faces_placeholder": "搜尋人臉..." + }, + "select_image_faces": { + "search_images_placeholder": "搜尋圖片..." + } + } + }, + "settings": { + "users": { + "table": { + "column_names": { + "admin": "管理員" + } + } + } + }, + "sidebar": { + "album": { + "cover_photo": "相簿封面", + "title": "相簿選項" + }, + "download": { + "filesize": { + "byte": "{{count}} 位元組", + "byte_plural": "{{count}} 位元組", + "giga_byte": "{{count}} GB", + "giga_byte_plural": "{{count}} GB", + "kilo_byte": "{{count}} KB", + "kilo_byte_plural": "{{count}} KB", + "mega_byte": "{{count}} MB", + "mega_byte_plural": "{{count}} MB", + "tera_byte": "{{count}} TB", + "tera_byte_plural": "{{count}} TB" + } + }, + "media": { + "album": "相簿" + }, + "sharing": { + "table_header": "公開分享" + } + } +} diff --git a/ui/src/favicon.ico b/ui/src/favicon.ico new file mode 100644 index 0000000..add7d3e Binary files /dev/null and b/ui/src/favicon.ico differ diff --git a/ui/src/favicon.svg b/ui/src/favicon.svg new file mode 100644 index 0000000..5124deb --- /dev/null +++ b/ui/src/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/helpers/authentication.test.ts b/ui/src/helpers/authentication.test.ts new file mode 100644 index 0000000..c02c1f1 --- /dev/null +++ b/ui/src/helpers/authentication.test.ts @@ -0,0 +1,73 @@ +import Cookies from 'js-cookie' + +import { + saveTokenCookie, + clearTokenCookie, + authToken, + saveSharePassword, + clearSharePassword, + getSharePassword, +} from './authentication' + +function resetCookies() { + Object.keys(Cookies.get()).forEach(function (cookieName) { + Cookies.remove(cookieName) + }) +} + +describe('helpers/authentication', () => { + afterEach(() => { + resetCookies() + }) + + test('auth token cookie operations', function () { + const AUTH_TOKEN = 'MOCK_TOKEN_1234' + + saveTokenCookie(AUTH_TOKEN) + + expect(document.cookie).toEqual(`auth-token=${AUTH_TOKEN}`) + + expect(authToken()).toEqual(AUTH_TOKEN) + + clearTokenCookie() + + expect(authToken()).toBeUndefined() + expect(document.cookie).toEqual('') + }) + + test('share token cookie operations', function () { + const SHARES = { + MOCK_SHARE_TOKEN1: 'simple', + MOCK_SHARE_TOKEN2: 'has-some-special-characters-!23@123', + MOCK_SHARE_TOKEN3: '\\', + } + + for (const shareToken in SHARES) { + const password = SHARES[shareToken as keyof typeof SHARES] + saveSharePassword(shareToken, password) + } + + expect(document.cookie).toEqual( + 'share-token-pw-MOCK_SHARE_TOKEN1=simple; share-token-pw-MOCK_SHARE_TOKEN2=has-some-special-characters-!23@123; share-token-pw-MOCK_SHARE_TOKEN3=%5C' + ) + + expect(getSharePassword('MOCK_SHARE_TOKEN1')).toEqual( + SHARES.MOCK_SHARE_TOKEN1 + ) + expect(getSharePassword('MOCK_SHARE_TOKEN2')).toEqual( + SHARES.MOCK_SHARE_TOKEN2 + ) + expect(getSharePassword('MOCK_SHARE_TOKEN3')).toEqual( + SHARES.MOCK_SHARE_TOKEN3 + ) + + clearSharePassword('MOCK_SHARE_TOKEN1') + clearSharePassword('MOCK_SHARE_TOKEN2') + clearSharePassword('MOCK_SHARE_TOKEN3') + + expect(getSharePassword('MOCK_SHARE_TOKEN1')).toBeUndefined() + expect(getSharePassword('MOCK_SHARE_TOKEN2')).toBeUndefined() + expect(getSharePassword('MOCK_SHARE_TOKEN3')).toBeUndefined() + expect(document.cookie).toEqual('') + }) +}) diff --git a/ui/src/helpers/authentication.ts b/ui/src/helpers/authentication.ts new file mode 100644 index 0000000..ac0dd3d --- /dev/null +++ b/ui/src/helpers/authentication.ts @@ -0,0 +1,47 @@ +import Cookies from 'js-cookie' + +const COOKIE_DEFAULT_OPTIONS = { + path: '/', + sameSite: 'Lax', +} as Partial + +const AUTH_TOKEN_MAX_AGE_IN_DAYS = 14 +const AUTH_TOKEN_COOKIE_NAME = 'auth-token' + +const SHARE_TOKEN_COOKIE_NAME = (shareToken: string) => + `share-token-pw-${shareToken}` + +export function saveTokenCookie(token: string) { + const options = { + ...COOKIE_DEFAULT_OPTIONS, + expires: AUTH_TOKEN_MAX_AGE_IN_DAYS, + } + + Cookies.set(AUTH_TOKEN_COOKIE_NAME, token, options) +} + +export function clearTokenCookie() { + Cookies.remove(AUTH_TOKEN_COOKIE_NAME) +} + +export function authToken() { + return Cookies.get(AUTH_TOKEN_COOKIE_NAME) +} + +export function saveSharePassword(shareToken: string, password: string) { + const cookieName = SHARE_TOKEN_COOKIE_NAME(shareToken) + + Cookies.set(cookieName, password, COOKIE_DEFAULT_OPTIONS) +} + +export function clearSharePassword(shareToken: string) { + const cookieName = SHARE_TOKEN_COOKIE_NAME(shareToken) + + Cookies.remove(cookieName) +} + +export function getSharePassword(shareToken: string) { + const cookieName = SHARE_TOKEN_COOKIE_NAME(shareToken) + + return Cookies.get(cookieName) +} diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts new file mode 100644 index 0000000..0aa1e34 --- /dev/null +++ b/ui/src/helpers/utils.ts @@ -0,0 +1,50 @@ +import classNames, { Argument as ClassNamesArg } from 'classnames' +import { overrideTailwindClasses } from 'tailwind-override' + +export interface DebouncedFn unknown> { + (...args: Parameters): void + cancel(): void +} + +export function debounce unknown>( + func: F, + wait: number, + triggerRising?: boolean +): DebouncedFn { + let timeout: number | undefined = undefined + + const debounced = (...args: Parameters) => { + if (timeout) { + clearTimeout(timeout) + timeout = undefined + } else if (triggerRising) { + func(...args) + } + + timeout = window.setTimeout(() => { + timeout = undefined + func(...args) + }, wait) + } + + debounced.cancel = () => { + clearTimeout(timeout) + timeout = undefined + } + + return debounced +} + +export function isNil(value: unknown): value is undefined | null { + return value === undefined || value === null +} + +export function exhaustiveCheck(value: never) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Exhaustive check failed with value: ${value}`) +} + +export function tailwindClassNames(...args: ClassNamesArg[]) { + return overrideTailwindClasses(classNames(args)) + // return classNames(args) +} diff --git a/ui/src/hooks/__mocks__/useScrollPagination.ts b/ui/src/hooks/__mocks__/useScrollPagination.ts new file mode 100644 index 0000000..ce051d7 --- /dev/null +++ b/ui/src/hooks/__mocks__/useScrollPagination.ts @@ -0,0 +1,4 @@ +export default () => ({ + finished: true, + containerElem: vi.fn(), +}) diff --git a/ui/src/hooks/useDelay.ts b/ui/src/hooks/useDelay.ts new file mode 100644 index 0000000..604c3f8 --- /dev/null +++ b/ui/src/hooks/useDelay.ts @@ -0,0 +1,23 @@ +import { useLayoutEffect, useState, useRef } from 'react' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function useDelay(wait: number, deps: any[] = []) { + const triggerUpdate = useState(false)[1] + const done = useRef(false) + + useLayoutEffect(() => { + const handle = setTimeout(() => { + done.current = true + triggerUpdate(x => !x) + }, wait) + + return () => { + done.current = false + clearTimeout(handle) + } + }, deps) + + return done.current +} + +export default useDelay diff --git a/ui/src/hooks/useOrderingParams.ts b/ui/src/hooks/useOrderingParams.ts new file mode 100644 index 0000000..9f85636 --- /dev/null +++ b/ui/src/hooks/useOrderingParams.ts @@ -0,0 +1,49 @@ +import { useCallback } from 'react' +import { OrderDirection } from '../__generated__/globalTypes' +import { UrlKeyValuePair, UrlParams } from './useURLParameters' + +export type MediaOrdering = { + orderBy: string | null + orderDirection: OrderDirection | null +} + +export type SetOrderingFn = (args: { + orderBy?: string + orderDirection?: OrderDirection +}) => void + +function useOrderingParams( + { getParam, setParams }: UrlParams, + defaultOrderBy = 'date_shot' +) { + const rawOrderBy = getParam('orderBy', defaultOrderBy) + const orderBy = rawOrderBy === null || rawOrderBy === '' ? defaultOrderBy : rawOrderBy + + const rawOrderDir = getParam('orderDirection', OrderDirection.ASC) + const orderDirection = + Object.values(OrderDirection).includes(rawOrderDir as OrderDirection) + ? (rawOrderDir as OrderDirection) : OrderDirection.ASC + + const setOrdering: SetOrderingFn = useCallback( + ({ orderBy, orderDirection }) => { + const updatedParams: UrlKeyValuePair[] = [] + if (orderBy !== undefined) { + updatedParams.push({ key: 'orderBy', value: orderBy }) + } + if (orderDirection !== undefined) { + updatedParams.push({ key: 'orderDirection', value: orderDirection }) + } + + setParams(updatedParams) + }, + [setParams] + ) + + return { + orderBy, + orderDirection, + setOrdering, + } +} + +export default useOrderingParams diff --git a/ui/src/hooks/useScrollPagination.ts b/ui/src/hooks/useScrollPagination.ts new file mode 100644 index 0000000..555383f --- /dev/null +++ b/ui/src/hooks/useScrollPagination.ts @@ -0,0 +1,99 @@ +import { ApolloQueryResult } from '@apollo/client' +import { useCallback, useEffect, useRef, useState } from 'react' + +interface ScrollPaginationArgs { + loading: boolean + data: D | undefined + fetchMore: (args: { + variables: { offset: number } + }) => Promise> + getItems: (data: D) => unknown[] +} + +type ScrollPaginationResult = { + finished: boolean + containerElem: (node: null | Element) => void +} + +const useScrollPagination: ( + args: ScrollPaginationArgs +) => ScrollPaginationResult = ({ loading, fetchMore, data, getItems }) => { + const observer = useRef(null) + const observerElem = useRef(null) + const [finished, setFinished] = useState(false) + + const reconfigureIntersectionObserver = () => { + const options = { + root: null, + rootMargin: '-100% 0px 0px 0px', + threshold: 0, + } + + // delete old observer + observer.current?.disconnect() + + if (finished) return + + // configure new observer + observer.current = new IntersectionObserver(entities => { + if (entities.find(x => x.isIntersecting == false)) { + const itemCount = data !== undefined ? getItems(data).length : 0 + fetchMore({ + variables: { + offset: itemCount, + }, + }).then(result => { + const newItemCount = getItems(result.data).length + if (newItemCount == 0) { + setFinished(true) + } + }) + } + }, options) + + // activate new observer + if (observerElem.current && !loading) { + observer.current.observe(observerElem.current) + } + } + + const containerElem = useCallback((node: null | Element): void => { + observerElem.current = node + + // cleanup + if (observer.current != null) { + observer.current.disconnect() + } + + if (node != null) { + reconfigureIntersectionObserver() + } + }, []) + + // only observe when not loading + useEffect(() => { + if (observer.current && observerElem.current) { + if (loading) { + observer.current.unobserve(observerElem.current) + } else { + observer.current.observe(observerElem.current) + } + } + }, [loading]) + + // reconfigure observer if fetchMore function changes + useEffect(() => { + reconfigureIntersectionObserver() + }, [fetchMore, data, finished]) + + useEffect(() => { + setFinished(false) + }, [data]) + + return { + containerElem, + finished, + } +} + +export default useScrollPagination diff --git a/ui/src/hooks/useURLParameters.ts b/ui/src/hooks/useURLParameters.ts new file mode 100644 index 0000000..328685d --- /dev/null +++ b/ui/src/hooks/useURLParameters.ts @@ -0,0 +1,57 @@ +import { useState } from 'react' + +export type UrlKeyValuePair = { key: string; value: string | null } + +export type UrlParams = { + getParam: (key: string, defaultValue?: string | null) => string | null + setParam: (key: string, value: string | null) => void + setParams: (pairs: UrlKeyValuePair[]) => void +} + +const useURLParameters: () => UrlParams = () => { + const [urlString, setUrlString] = useState(document.location.href) + + const url = new URL(urlString) + const params = new URLSearchParams(url.search) + + const getParam = (key: string, defaultValue: string | null = null) => { + return params.has(key) ? params.get(key) : defaultValue + } + + const updateParams = () => { + if (params.toString()) { + history.replaceState({}, '', url.pathname + '?' + params.toString()) + } else { + history.replaceState({}, '', url.pathname) + } + setUrlString(document.location.href) + } + + const setParam = (key: string, value: string | null) => { + if (value) { + params.set(key, value) + } else { + params.delete(key) + } + updateParams() + } + + const setParams = (pairs: UrlKeyValuePair[]) => { + for (const pair of pairs) { + if (pair.value) { + params.set(pair.key, pair.value) + } else { + params.delete(pair.key) + } + } + updateParams() + } + + return { + getParam, + setParam, + setParams, + } +} + +export default useURLParameters diff --git a/ui/src/index.css b/ui/src/index.css new file mode 100644 index 0000000..e04f530 --- /dev/null +++ b/ui/src/index.css @@ -0,0 +1,68 @@ +/* ===== Custom Fonts ===== */ +@font-face { + font-family: 'iA Writer Quattro S'; + src: url('/fonts/iAWriterQuattroSRegular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Noto Sans SC'; + src: url('/fonts/noto-sans-sc-400.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Noto Sans SC'; + src: url('/fonts/noto-sans-sc-500.ttf') format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Noto Sans SC'; + src: url('/fonts/noto-sans-sc-700.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root, +body, +html { + margin: 0; + font-size: inherit; + font-family: 'iA Writer Quattro S', 'Noto Sans SC', sans-serif; +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + .flip-y { + transform: scale(1, -1); + } +} + +@layer base { + body.dark, + html.dark { + @apply bg-dark-bg text-dark-text; + } + + body.dark *, + html.dark * { + @apply border-dark-border; + } + + img { + max-width: revert; + } +} diff --git a/ui/src/index.tsx b/ui/src/index.tsx new file mode 100644 index 0000000..baba542 --- /dev/null +++ b/ui/src/index.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import client from './apolloClient' +import { ApolloProvider } from '@apollo/client' +import { BrowserRouter as Router } from 'react-router-dom' +import { setupLocalization } from './localization' +import { updateTheme } from './theme' +import * as serviceWorkerRegistration from './serviceWorkerRegistration' + +import './index.css' +import { SidebarProvider } from './components/sidebar/Sidebar' + +updateTheme() +setupLocalization() + +const Main = () => ( + + + + + + + +) + +const root = createRoot(document.getElementById('root')!) +root.render(
    ) + +// serviceWorkerRegistration.register() diff --git a/ui/src/localization.ts b/ui/src/localization.ts new file mode 100644 index 0000000..fb079f3 --- /dev/null +++ b/ui/src/localization.ts @@ -0,0 +1,181 @@ +import { useEffect } from 'react' +import { siteTranslation } from './__generated__/siteTranslation' +import { gql, useLazyQuery } from '@apollo/client' +import i18n from 'i18next' +import { initReactI18next, TFunction } from 'react-i18next' +import { LanguageTranslation } from './__generated__/globalTypes' +import { authToken } from './helpers/authentication' +import { exhaustiveCheck, isNil } from './helpers/utils' + +export type TranslationFn = TFunction<'translation'> + +export function setupLocalization(): void { + i18n + .use(initReactI18next) + .init({ + lng: 'en', + fallbackLng: 'en', + returnNull: false, + returnEmptyString: false, + + interpolation: { + escapeValue: false, + }, + + react: { + useSuspense: import.meta.env.PROD, + }, + }) + .catch(err => console.error('Failed to setup localization', err)) +} + +const SITE_TRANSLATION = gql` + query siteTranslation { + myUserPreferences { + id + language + } + } +` + +export const loadTranslations = () => { + const [loadLang, { data }] = useLazyQuery(SITE_TRANSLATION) + + useEffect(() => { + if (authToken()) { + loadLang() + } + }, [authToken()]) + + useEffect(() => { + const language = data?.myUserPreferences.language + if (isNil(language)) { + i18n.changeLanguage('en') + return + } + + switch (language) { + case LanguageTranslation.Danish: + import('./extractedTranslations/da/translation.json').then(language => { + i18n.addResourceBundle('da', 'translation', language) + i18n.changeLanguage('da') + }) + return + case LanguageTranslation.Dutch: + import('./extractedTranslations/nl/translation.json').then(language => { + i18n.addResourceBundle('nl', 'translation', language) + i18n.changeLanguage('nl') + }) + return + case LanguageTranslation.English: + import('./extractedTranslations/en/translation.json').then(language => { + i18n.addResourceBundle('en', 'translation', language) + i18n.changeLanguage('en') + }) + return + case LanguageTranslation.French: + import('./extractedTranslations/fr/translation.json').then(language => { + i18n.addResourceBundle('fr', 'translation', language) + i18n.changeLanguage('fr') + }) + return + case LanguageTranslation.Swedish: + import('./extractedTranslations/sv/translation.json').then(language => { + i18n.addResourceBundle('sv', 'translation', language) + i18n.changeLanguage('sv') + }) + return + case LanguageTranslation.Italian: + import('./extractedTranslations/it/translation.json').then(language => { + i18n.addResourceBundle('it', 'translation', language) + i18n.changeLanguage('it') + }) + return + case LanguageTranslation.Spanish: + import('./extractedTranslations/es/translation.json').then(language => { + i18n.addResourceBundle('es', 'translation', language) + i18n.changeLanguage('es') + }) + return + case LanguageTranslation.Polish: + import('./extractedTranslations/pl/translation.json').then(language => { + i18n.addResourceBundle('pl', 'translation', language) + i18n.changeLanguage('pl') + }) + return + case LanguageTranslation.Ukrainian: + import('./extractedTranslations/uk/translation.json').then(language => { + i18n.addResourceBundle('uk', 'translation', language) + i18n.changeLanguage('uk') + }) + return + case LanguageTranslation.German: + import('./extractedTranslations/de/translation.json').then(language => { + i18n.addResourceBundle('de', 'translation', language) + i18n.changeLanguage('de') + }) + return + case LanguageTranslation.Russian: + import('./extractedTranslations/ru/translation.json').then(language => { + i18n.addResourceBundle('ru', 'translation', language) + i18n.changeLanguage('ru') + }) + return + case LanguageTranslation.TraditionalChineseTW: + import('./extractedTranslations/zh-TW/translation.json').then( + language => { + i18n.addResourceBundle('zh-TW', 'translation', language) + i18n.changeLanguage('zh-TW') + } + ) + return + case LanguageTranslation.TraditionalChineseHK: + import('./extractedTranslations/zh-HK/translation.json').then( + language => { + i18n.addResourceBundle('zh-HK', 'translation', language) + i18n.changeLanguage('zh-HK') + } + ) + return + case LanguageTranslation.SimplifiedChinese: + import('./extractedTranslations/zh-CN/translation.json').then( + language => { + i18n.addResourceBundle('zh-CN', 'translation', language) + i18n.changeLanguage('zh-CN') + } + ) + return + case LanguageTranslation.Portuguese: + import('./extractedTranslations/pt/translation.json').then(language => { + i18n.addResourceBundle('pt', 'translation', language) + i18n.changeLanguage('pt') + }) + return + case LanguageTranslation.Basque: + import('./extractedTranslations/eu/translation.json').then(language => { + i18n.addResourceBundle('eu', 'translation', language) + i18n.changeLanguage('eu') + }) + return + case LanguageTranslation.Turkish: + import('./extractedTranslations/tr/translation.json').then(language => { + i18n.addResourceBundle('tr', 'translation', language) + i18n.changeLanguage('tr') + }) + return + case LanguageTranslation.Japanese: + import('./extractedTranslations/ja/translation.json').then(language => { + i18n.addResourceBundle('ja', 'translation', language) + i18n.changeLanguage('ja') + }) + return + default: + import('./extractedTranslations/en/translation.json').then(language => { + i18n.addResourceBundle('en', 'translation', language) + i18n.changeLanguage('en') + }) + // Throw the error if we have an unhandled language + exhaustiveCheck(language) + } + }, [data?.myUserPreferences.language]) +} diff --git a/ui/src/manifest.webmanifest b/ui/src/manifest.webmanifest new file mode 100644 index 0000000..36ff4e9 --- /dev/null +++ b/ui/src/manifest.webmanifest @@ -0,0 +1,23 @@ +{ + "name": "Photoview", + "short_name": "Photoview", + "description": "Photo gallery for self-hosted personal servers", + "start_url": "/", + "display": "standalone", + "theme_color": "black", + "background_color": "white", + "icons": [ + { + "src": "/assets/logo192.png", + "sizes": "192x192" + }, + { + "src": "/assets/logo512.png", + "sizes": "512x512" + }, + { + "src": "/assets/photoview-logo.svg", + "sizes": "150x150" + } + ] +} diff --git a/ui/src/primitives/Loader.tsx b/ui/src/primitives/Loader.tsx new file mode 100644 index 0000000..edb1004 --- /dev/null +++ b/ui/src/primitives/Loader.tsx @@ -0,0 +1,20 @@ +import React, { CSSProperties } from 'react' + +type LoaderProps = { + active: boolean + message?: string + size?: 'small' | 'default' + className?: string + style?: CSSProperties +} + +const Loader = ({ active, message, className, style }: LoaderProps) => { + if (!active) return null + return ( +
    + {message ?? 'Loading...'} +
    + ) +} + +export default Loader diff --git a/ui/src/primitives/Modal.tsx b/ui/src/primitives/Modal.tsx new file mode 100644 index 0000000..0f097ba --- /dev/null +++ b/ui/src/primitives/Modal.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import { Dialog } from '@headlessui/react' +import { Button } from './form/Input' + +export type ModalAction = { + key: string + label: string + variant?: 'negative' | 'positive' | 'default' + onClick(event: React.MouseEvent): void +} + +export type ModalProps = { + title: string + description: React.ReactNode + children?: React.ReactNode + actions: ModalAction[] + open: boolean + onClose(): void +} + +const Modal = ({ + title, + description, + children, + actions, + open, + onClose, +}: ModalProps) => { + const actionElms = actions.map(x => ( + + )) + + return ( + +
    + + +
    +
    + {title} + + {description} + + + {children} +
    + +
    + {actionElms} +
    +
    +
    +
    + ) +} + +export default Modal diff --git a/ui/src/primitives/Table.tsx b/ui/src/primitives/Table.tsx new file mode 100644 index 0000000..b7a111d --- /dev/null +++ b/ui/src/primitives/Table.tsx @@ -0,0 +1,46 @@ +import styled from 'styled-components' + +export const Table = styled.table.attrs({ + className: 'border dark:border-dark-border border-separate rounded' as string, +})` + border-spacing: 0; + + & td:not(:last-child), + & th:not(:last-child) { + border-right: 1px solid; + border-color: inherit; + } + + & tr:first-child td { + border-top: 1px solid; + border-color: inherit; + } + + & td { + border-bottom: 1px solid; + border-color: inherit; + } +` + +export const TableHeader = styled.thead.attrs({ + className: 'text-left', +})`` + +export const TableBody = styled.tbody.attrs({ className: '' as string })`` + +export const TableFooter = styled.tfoot.attrs({ className: '' as string })`` + +export const TableRow = styled.tr.attrs({ className: '' as string })`` + +export const TableCell = styled.td.attrs({ + className: 'py-2 px-2 align-top dark:bg-[#2a2f35]' as string, +})`` + +export const TableHeaderCell = styled.th.attrs({ + className: + 'bg-gray-50 dark:bg-dark-bg2 py-2 px-2 align-top font-semibold' as string, +})`` + +export const TableScrollWrapper = styled.div.attrs({ + className: 'block overflow-x-auto whitespace-nowrap' as string, +})`` diff --git a/ui/src/primitives/form/Checkbox.tsx b/ui/src/primitives/form/Checkbox.tsx new file mode 100644 index 0000000..6ac05a9 --- /dev/null +++ b/ui/src/primitives/form/Checkbox.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import styled from 'styled-components' + +const CheckboxLabelWrapper = styled.label` + & input[type='checkbox'] { + appearance: none; + outline: none; + } + + & input[type='checkbox'] + span { + cursor: pointer; + } + + & input[type='checkbox'] + span::before { + content: ''; + background-repeat: no-repeat; + background-position: center; + width: 12px; + height: 12px; + background-color: #f9fafb; + border: 1px solid #aaa; + display: inline-block; + border-radius: 3px; + cursor: pointer; + margin-right: 8px; + margin-bottom: -1px; + } + + & input[type='checkbox']:checked + span::before { + background-color: #0072ff; + border-color: #109dff; + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='7px' height='4px' viewBox='0 0 7 4' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EPath 5%3C/title%3E%3Cg stroke='none' stroke-width='1.5' fill='none' fill-rule='evenodd'%3E%3Cpolyline stroke='white' points='1 1.93487039 2.77174339 3.70661378 6.47835718 2.27373675e-13'%3E%3C/polyline%3E%3C/g%3E%3C/svg%3E"); + } + + & input[type='checkbox']:focus + span::before { + outline: 2px solid #86c6ff; + border-color: #109dff; + } + + & input[type='checkbox']:disabled + span::before { + border-color: #ccc; + background-color: #fefefe; + cursor: initial; + } + + & input[type='checkbox']:disabled:checked + span::before { + background-color: #eee; + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='7px' height='4px' viewBox='0 0 7 4' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EPath 5%3C/title%3E%3Cg stroke='none' stroke-width='1.5' fill='none' fill-rule='evenodd'%3E%3Cpolyline stroke='%23444444' points='1 1.93487039 2.77174339 3.70661378 6.47835718 2.27373675e-13'%3E%3C/polyline%3E%3C/g%3E%3C/svg%3E"); + } + + & input[type='checkbox']:disabled + span { + color: #555; + cursor: initial; + } +` + +type CheckboxProps = React.InputHTMLAttributes & { + label: string + className?: string +} + +const Checkbox = ({ label, className, ...props }: CheckboxProps) => { + return ( + + + {label} + + ) +} + +export default Checkbox diff --git a/ui/src/primitives/form/Dropdown.tsx b/ui/src/primitives/form/Dropdown.tsx new file mode 100644 index 0000000..c68dc96 --- /dev/null +++ b/ui/src/primitives/form/Dropdown.tsx @@ -0,0 +1,59 @@ +import classNames from 'classnames' +import React from 'react' +import styled from 'styled-components' + +const DropdownStyledSelect = styled.select` + appearance: none; + + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9px' height='5px' viewBox='0 0 9 5'%3E%3Cpolygon fill='%23D8D8D8' points='0 0 8.36137659 0 4.1806883 4.1806883'%3E%3C/polygon%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center right 10px; +` + +export type DropdownItem = { + value: string + label: string +} + +type DropdownProps = React.SelectHTMLAttributes & { + items: DropdownItem[] + selected?: string + setSelected(value: string): void + className?: string +} + +const Dropdown = ({ + items, + selected, + setSelected, + className, + ...otherProps +}: DropdownProps) => { + const onChange = (e: React.ChangeEvent) => { + setSelected(e.target.value) + otherProps.onChange && otherProps.onChange(e) + } + + const options = items.map(({ value, label }) => ( + + )) + + return ( + + {options} + + ) +} + +export default Dropdown diff --git a/ui/src/primitives/form/Input.tsx b/ui/src/primitives/form/Input.tsx new file mode 100644 index 0000000..bc75ab3 --- /dev/null +++ b/ui/src/primitives/form/Input.tsx @@ -0,0 +1,200 @@ +import React, { forwardRef } from 'react' +import classNames, { Argument as ClassNamesArg } from 'classnames' +import { ReactComponent as ActionArrowIcon } from './icons/textboxActionArrow.svg' +import { ReactComponent as LoadingSpinnerIcon } from './icons/textboxLoadingSpinner.svg' +import styled from 'styled-components' +import { tailwindClassNames } from '../../helpers/utils' + +type TextFieldProps = { + label?: string + error?: string + className?: ClassNamesArg + wrapperClassName?: ClassNamesArg + sizeVariant?: 'default' | 'big' + fullWidth?: boolean + action?: () => void + loading?: boolean +} & Omit, 'className'> + +export const TextField = forwardRef( + ( + { + label, + error, + className, + wrapperClassName, + sizeVariant, + fullWidth, + action, + loading, + ...inputProps + }: TextFieldProps, + ref: React.ForwardedRef + ) => { + const disabled = !!inputProps.disabled + sizeVariant = sizeVariant ?? 'default' + + let variant = 'bg-white border-gray-200 focus:border-blue-400' + if (error) + variant = + 'bg-red-50 border-red-200 focus:border-red-400 focus:ring-red-100 placeholder-red-300' + + if (disabled) variant = 'bg-gray-100' + + let keyUpEvent = undefined + if (action) { + keyUpEvent = (event: React.KeyboardEvent) => { + if (inputProps.onKeyUp) inputProps.onKeyUp(event) + + if (event.key == 'Enter') { + event.preventDefault() + action() + } + } + } + + let input = ( + + ) + + if (loading) { + input = ( +
    + {input} + +
    + ) + } else if (action) { + input = ( +
    + {input} + +
    + ) + } + + let errorElm = null + if (error) errorElm =
    {error}
    + + const wrapperClasses = classNames( + sizeVariant == 'default' && 'text-sm', + wrapperClassName + ) + + if (label) { + return ( + + ) + } + + return ( +
    + {input} + {errorElm} +
    + ) + } +) + +type ButtonProps = { + variant?: 'negative' | 'positive' | 'default' + background?: 'default' | 'white' + className?: string +} + +export const buttonStyles = ({ variant, background }: ButtonProps) => + classNames( + 'px-6 py-0.5 rounded border border-gray-200 focus:outline-none focus:border-blue-300 text-[#222] hover:bg-gray-100 whitespace-nowrap', + 'dark:bg-dark-input-bg dark:border-dark-input-border dark:text-dark-input-text dark:focus:border-blue-300', + variant == 'negative' && + 'text-red-600 hover:bg-red-600 hover:border-red-700 hover:text-white transition-colors focus:border-red-600 focus:hover:border-red-700', + variant == 'positive' && + 'text-green-600 hover:bg-green-600 hover:border-green-700 hover:text-white transition-colors focus:border-green-600 focus:hover:border-green-700', + background == 'white' ? 'bg-white' : 'bg-gray-50' + ) + +type SubmitProps = ButtonProps & { + children: string + className?: string +} + +export const Submit = ({ + className, + variant, + background, + children, + ...props +}: SubmitProps & React.ButtonHTMLAttributes) => ( + +) + +export const Button = ({ + children, + variant, + background, + className, + ...props +}: ButtonProps & React.ButtonHTMLAttributes) => ( + +) + +export const ButtonGroup = styled.div.attrs({ className: 'flex gap-1' })`` diff --git a/ui/src/primitives/form/MessageBox.tsx b/ui/src/primitives/form/MessageBox.tsx new file mode 100644 index 0000000..8453da0 --- /dev/null +++ b/ui/src/primitives/form/MessageBox.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +type MessageBoxProps = { + message?: string | null + show?: boolean + type?: 'neutral' | 'positive' | 'negative' +} + +const MessageBox = ({ message, show, type }: MessageBoxProps) => { + if (!show) return null + + let variant = 'bg-gray-100' + if (type == 'positive') variant = 'bg-green-200 text-green-900' + if (type == 'negative') variant = 'bg-red-200 text-red-900' + + return
    {message}
    +} + +export default MessageBox diff --git a/ui/src/primitives/form/icons/textboxActionArrow.svg b/ui/src/primitives/form/icons/textboxActionArrow.svg new file mode 100644 index 0000000..72741cc --- /dev/null +++ b/ui/src/primitives/form/icons/textboxActionArrow.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/primitives/form/icons/textboxLoadingSpinner.svg b/ui/src/primitives/form/icons/textboxLoadingSpinner.svg new file mode 100644 index 0000000..b9b08a4 --- /dev/null +++ b/ui/src/primitives/form/icons/textboxLoadingSpinner.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/service-worker.ts b/ui/src/service-worker.ts new file mode 100644 index 0000000..7ea8ebc --- /dev/null +++ b/ui/src/service-worker.ts @@ -0,0 +1,87 @@ +/// +/* eslint-disable no-restricted-globals */ + +// This service worker can be customized! +// See https://developers.google.com/web/tools/workbox/modules +// for the list of available Workbox modules, or add any other +// code you'd like. +// You can also remove this file if you'd prefer not to use a +// service worker, and the Workbox build step will be skipped. + +import { clientsClaim } from 'workbox-core' +import { ExpirationPlugin } from 'workbox-expiration' +import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching' +import { registerRoute } from 'workbox-routing' +import { StaleWhileRevalidate } from 'workbox-strategies' + +declare const self: ServiceWorkerGlobalScope + +clientsClaim() + +// Precache all of the assets generated by your build process. +// Their URLs are injected into the manifest variable below. +// This variable must be present somewhere in your service worker file, +// even if you decide not to use precaching. See https://cra.link/PWA +precacheAndRoute(self.__WB_MANIFEST) + +// Set up App Shell-style routing, so that all navigation requests +// are fulfilled with your index.html shell. Learn more at +// https://developers.google.com/web/fundamentals/architecture/app-shell +const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$') +registerRoute( + // Return false to exempt requests from being fulfilled by index.html. + ({ request, url }: { request: Request; url: URL }) => { + // If this isn't a navigation, skip. + if (request.mode !== 'navigate') { + return false + } + + // If this is a URL that starts with /_, skip. + if (url.pathname.startsWith('/_')) { + return false + } + + // Skip API requests + if (url.pathname.startsWith('/api/')) { + return false + } + + // If this looks like a URL for a resource, because it contains + // a file extension, skip. + if (url.pathname.match(fileExtensionRegexp)) { + return false + } + + // Return true to signal that we want to use the handler. + return true + }, + createHandlerBoundToURL(import.meta.env.BASE_URL + 'index.html') +) + +// An example runtime caching route for requests that aren't handled by the +// precache, in this case same-origin .png requests like those from in public/ +registerRoute( + // Add in any other file extensions or routing criteria as needed. + ({ url }) => + url.origin === self.location.origin && url.pathname.endsWith('.png'), + // Customize this strategy as needed, e.g., by changing to CacheFirst. + new StaleWhileRevalidate({ + cacheName: 'images', + plugins: [ + // Ensure that once this runtime cache reaches a maximum size the + // least-recently used images are removed. + new ExpirationPlugin({ maxEntries: 50 }), + ], + }) +) + +// This allows the web app to trigger skipWaiting via +// registration.waiting.postMessage({type: 'SKIP_WAITING'}) +self.addEventListener('message', event => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } +}) + +// Any other custom service worker logic can go here. diff --git a/ui/src/serviceWorkerRegistration.ts b/ui/src/serviceWorkerRegistration.ts new file mode 100644 index 0000000..d32dcad --- /dev/null +++ b/ui/src/serviceWorkerRegistration.ts @@ -0,0 +1,150 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://cra.link/PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +) + +type Config = { + onSuccess?: (registration: ServiceWorkerRegistration) => void + onUpdate?: (registration: ServiceWorkerRegistration) => void +} + +export function register(config?: Config) { + if (import.meta.env.PROD && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(import.meta.env.BASE_URL, window.location.href) + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return + } + + window.addEventListener('load', () => { + const swUrl = `${import.meta.env.BASE_URL}service-worker.js` + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config) + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready + .then(() => { + console.log( + 'This web app is being served cache-first by a service worker. ' + + 'To learn more, visit https://cra.link/PWA' + ) + }) + .catch(() => { + console.log('Failed to load service worker') + }) + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config) + } + }) + } +} + +function registerValidSW(swUrl: string, config?: Config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing + if (installingWorker == null) { + return + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://cra.link/PWA.' + ) + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration) + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.') + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration) + } + } + } + } + } + }) + .catch(error => { + console.error('Error during service worker registration:', error) + }) +} + +function checkValidServiceWorker(swUrl: string, config?: Config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { 'Service-Worker': 'script' }, + }) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type') + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload() + }) + }) + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config) + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ) + }) +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then(registration => { + registration.unregister() + }) + .catch((error: Error) => { + console.error(error.message) + }) + } +} diff --git a/ui/src/theme.ts b/ui/src/theme.ts new file mode 100644 index 0000000..4d47542 --- /dev/null +++ b/ui/src/theme.ts @@ -0,0 +1,37 @@ +export const updateTheme = () => { + if ( + localStorage.theme === 'dark' || + (!('theme' in localStorage) && + window.matchMedia('(prefers-color-scheme: dark)').matches) + ) { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } +} + +export const changeTheme = (value: string) => { + if (value == 'light') { + localStorage.theme = 'light' + } else if (value == 'dark') { + localStorage.theme = 'dark' + } else { + // use OS preference + localStorage.removeItem('theme') + } + + updateTheme() +} + +export const getTheme = () => { + if (localStorage.theme == 'light') { + return 'light' + } else if (localStorage.theme == 'dark') { + return 'dark' + } else { + return 'auto' + } +} + +export const isDarkMode = () => + document.documentElement.classList.contains('dark') diff --git a/ui/src/vite.env.d.ts b/ui/src/vite.env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/ui/src/vite.env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/tailwind.config.js b/ui/tailwind.config.js new file mode 100644 index 0000000..5f0e5f4 --- /dev/null +++ b/ui/tailwind.config.js @@ -0,0 +1,46 @@ +module.exports = { + mode: 'jit', + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + purge: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + darkMode: 'class', + theme: { + extend: { + screens: { + xs: '480px', + }, + boxShadow: { + separator: '0 0 4px 0 rgba(0, 0, 0, 0.1)', + }, + colors: { + green: { + 50: '#f0ffee', + 100: '#dcffd8', + 200: '#b6fdb3', + 300: '#7cf587', + 400: '#56e263', + 500: '#4abf3c', + 600: '#30a23e', + 700: '#168332', + 800: '#006624', + 900: '#00541d', + }, + dark: { + bg: '#000000', + bg2: '#111111', + text: '#eee', + border: '#3b3b3b', + border2: '#202020', + input: { + bg: '#383e46', + border: '#4a515a', + text: '#ccdbe4', + }, + }, + }, + }, + }, + variants: { + extend: {}, + }, + plugins: [], +} diff --git a/ui/testing/setupTests.ts b/ui/testing/setupTests.ts new file mode 100644 index 0000000..10f4c03 --- /dev/null +++ b/ui/testing/setupTests.ts @@ -0,0 +1,11 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom' +import '@testing-library/user-event' + +import { setupLocalization } from '../src/localization' + +// setup localization to make it easier to select elements by text +setupLocalization() diff --git a/ui/testing/transform-svg.js b/ui/testing/transform-svg.js new file mode 100644 index 0000000..155c2a6 --- /dev/null +++ b/ui/testing/transform-svg.js @@ -0,0 +1,15 @@ +/** + * Mock .svg imports + */ + +const path = require('path') + +module.exports = { + process(_, filename) { + return 'module.exports = "' + path.basename(filename) + '.svg"' + }, + getCacheKey(_, filename) { + // The output is based on path. + return path.basename(filename) + }, +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..19d0249 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vitest/globals", "@testing-library/jest-dom"] + }, + "include": ["src", "vite.config.ts", ".eslintrc.js", "apollo.config.js"] +} diff --git a/ui/vite.config.js b/ui/vite.config.js new file mode 100644 index 0000000..5f50e47 --- /dev/null +++ b/ui/vite.config.js @@ -0,0 +1,29 @@ +/// +/// + +import { defineConfig } from 'vite' +import svgr from 'vite-plugin-svgr' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react(), svgr()], + envPrefix: ['VITE_', 'REACT_APP_'], + server: { + port: 1234, + }, + esbuild: { + logOverride: { 'this-is-undefined-in-esm': 'silent' }, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './testing/setupTests.ts', + reporters: ['verbose', 'junit'], + outputFile: { + junit: './junit-report.xml', + }, + coverage: { + reporter: ['text', 'json', 'html'], + }, + }, +}) \ No newline at end of file