commit 83131b9a178a5aad1dbe8bc9e32e26ba620d8383 Author: Thomas Egger Date: Thu May 25 23:57:05 2023 +0200 Initial upload diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1b73304 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.github +.git* + +*.md \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..39a6624 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,45 @@ +--- +name: Bug Report +about: Create a report to help us improve. Please check the FAQ first. +title: 'Bug:/Enhancement:/Feature Request: ' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Self-Hosted** +No | Yes + +**Self-Hosted Setup** +Proxy: Nginx | Apache2 +Deployment: docker run | docker-compose | npm run start:prod + +**Additional context** +Add any other context about the problem here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3a3cce5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..4af3021 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image + run: docker build --pull . -f Dockerfile -t moreload diff --git a/.github/workflows/github-image.yml b/.github/workflows/github-image.yml new file mode 100644 index 0000000..7a67bcb --- /dev/null +++ b/.github/workflows/github-image.yml @@ -0,0 +1,58 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# GitHub recommends pinning actions to a commit SHA. +# To get a newer version, you will need to update the SHA. +# You can also reference a tag or branch, but the action may change without warning. + +name: GHCR Image CI + +on: + push: + tags: + - "v*.*.*" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup qemu + uses: docker/setup-qemu-action@v2.1.0 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd15e97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +fqdn.env +/docker/certs +qrcode-svg/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..589a93c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Priorities +- MoreLoad should be extremely simple, clean, and easy to use. +- The main user flow should never be obstructed! +- New features must be tested thoroughly before we are able to merge them. +- Stability always comes first! + +# Agenda +MoreLoad is a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer. +We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity. + +If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555). + +# Contributing guidelines +Make sure to follow these guidelines before opening an [issue](https://github.com/moreload/moreload/issues/new/choose) or a [pull request](https://github.com/moreload/moreload/pulls): + +- Before opening an issue of a pull request, please check if the issue or the pull request already exists. +- Pull requests for packages updates are not allowed since there is [Dependabot](https://github.com/moreload/moreload/blob/master/.github/dependabot.yml) that checks them automatically. +- If you don't know how to contribute, also if you don't know JavaScript or Node.js, you can still share your awesome ideas with a new issue (feature request) and check the whole project for misspellings, too. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a307a45 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:lts-alpine + +WORKDIR /home/node/app + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb1d5f6 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +
+ + Logo + + +

MoreLoad

+ +

+ Local file sharing in your browser. Inspired by Apple's AirDrop. +
+ Explore » +
+
+ Report Bug + · + Request Feature +

+
+ +## Features +[MoreLoad](https://moreload.com) is a sublime alternative to AirDrop that works on all platforms. + +Send images, documents or text via peer to peer connection to devices in the same local network/Wi-Fi or to paired devices. +As it is web based, it runs on all devices. + +You want to quickly send a file from your phone to your laptop? +
You want to share photos in original quality with friends that use a mixture of Android and iOS? +
You want to share private files peer to peer between Linux systems? +
AirDrop is unreliable again? +
_Send it with MoreLoad!_ + +Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop) + +## Differences to Snapdrop + +### Device Pairing / Internet Transfer +* Pair devices via 6-digit code or QR-Code +* Pair devices outside your local network or in complex network environment (public Wi-Fi, company network, Apple Private Relay, VPN etc.). +* Connect to devices on your mobile hotspot. +* Paired devices will always find each other via shared secrets even after reopening the browser or the Progressive Web App +* You will always discover devices on your local network. Paired devices are shown additionally. +* Paired devices outside your local network that are behind a NAT are connected automatically via the MoreLoad TURN server. + +### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560) +* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible. +* Multiple files are downloaded as a ZIP file +* On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu. +* Multiple files are transferred at once with an overall progress indicator + +### Send Files or Text Directly From Share Menu, Context Menu or CLI +* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) +* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) +* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android) +* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) + +### Other changes +* Change your display name permanently to easily differentiate your devices +* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534) +* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413) +* Warn user before MoreLoad is closed on file transfer +* Open MoreLoad on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101)) +* [Video and Audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads)) +* Switch theme back to auto/system after darkmode or lightmode is enabled +* Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario)) +* Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87)) +* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101)) +* To host MoreLoad on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558) +* When hosting MoreLoad yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers) + +## MoreLoad is built with the following awesome technologies: +* Vanilla HTML5 / ES6 / CSS3 frontend +* [WebRTC](http://webrtc.org/) / [WebSockets](http://www.websocket.org/) +* [NodeJS](https://nodejs.org/en/) backend +* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App) +* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) +* [zip.js](https://gildas-lormeau.github.io/zip.js/) +* [cyrb53](https://github.com/bryc) super fast hash function + +Have any questions? Read our [FAQ](/docs/faq.md). + +You can [host your own instance with Docker](/docs/host-your-own.md). + + +## Support the Community +MoreLoad is free and always will be. + +To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop). + +## How to contribute + +Feel free to [open an issue](https://github.com/moreload/moreload/issues/new/choose) or a +[pull request](https://github.com/moreload/moreload/pulls) but follow +[Contributing Guidelines](/CONTRIBUTING.md). diff --git a/docker-compose-coturn.yml b/docker-compose-coturn.yml new file mode 100644 index 0000000..e9a05b4 --- /dev/null +++ b/docker-compose-coturn.yml @@ -0,0 +1,19 @@ +version: "3" +services: + node: + image: "node:lts-alpine" + user: "node" + working_dir: /home/node/app + volumes: + - ./:/home/node/app + command: ash -c "npm i && npm run start:prod" + restart: unless-stopped + ports: + - "3000:3000" + coturn_server: + image: "coturn/coturn" + restart: always + network_mode: "host" + volumes: + - ./turnserver.conf:/etc/coturn/turnserver.conf + #you need to copy turnserver_example.conf to turnserver.conf and specify domain, IP address, user and password diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5ff9305 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" +services: + node: + image: "node:lts-alpine" + user: "node" + working_dir: /home/node/app + volumes: + - ./:/home/node/app + command: ash -c "npm i && npm run start:prod" + restart: unless-stopped + ports: + - "3000:3000" diff --git a/docs/docker-swarm-usage.md b/docs/docker-swarm-usage.md new file mode 100644 index 0000000..ae2c97e --- /dev/null +++ b/docs/docker-swarm-usage.md @@ -0,0 +1,43 @@ +# Docker Swarm Usage + +## Healthcheck + +The [Docker Image](../Dockerfile) includes a Healthcheck with the following options: + +``` +--interval=30s +``` +> Specifies the time interval at which the health check should be performed. In this case, the health check will be performed every 30 seconds. + +
+ +``` +--timeout=10s +``` +> Specifies the amount of time to wait for a response from the health check command. If the response does not arrive within 10 seconds, the health check will be considered a failure. + +
+ +``` +--start-period=5s +``` +> Specifies the amount of time to wait before starting the health check process. In this case, the health check process will begin 5 seconds after the container is started. + +
+ +``` +--retries=3 +``` +> Specifies the number of times Docker should retry the health check before considering the container to be unhealthy. + +
+ + +The CMD instruction is used to define the command that will be run as part of the health check. +In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. This command will attempt to connect to `http://localhost:3000/` +and if it fails it will exit with a status code of `1`. If this command returns a status code other than `0`, the health check will be considered a failure. + +Overall, this HEALTHCHECK instruction is defining a health check process that will run every 30 seconds, wait up to 10 seconds for a response, +begin 5 seconds after the container is started, and retry up to 3 times. +The health check will consist of attempting to connect to http://localhost:3000/ and will consider the container to be unhealthy if it is unable to connect. + diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..ac2e579 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,210 @@ +# Frequently Asked Questions + +
+ + Help! I can't install the PWA! + + +if you are using a Chromium-based browser (Chrome, Edge, Brave, etc.), you can easily install MoreLoad PWA on your desktop +by clicking the install-button in the top-right corner while on [moreload.com](https://moreload.com). + +Example on how to install a pwa with Edge + +On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/) + +
+ +Self-Hosted Instance? + +To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs). +See [this host your own section](https://github.com/MoreLoad/MoreLoad/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more information. + +
+ +
+ +
+ + Shortcuts? + + +Shortcuts! +- Send a message with `CTRL + ENTER` +- Close all send and pair dialogs by pressing `Escape`. +- Copy a received message to clipboard with `CTRL/⌘ + C`. +- Accept file transfer request with `Enter` and decline with `Escape`. + +
+ +
+ +
+ + How to save images directly to the gallery on iOS? + + +Apparently, iOS does not allow images shared from a website to be saved to the gallery directly. +It simply does not offer the option for images shared from a website. + +iOS Shortcuts to the win: +I created a simple iOS shortcut that takes your photos and saves them to your gallery: +https://routinehub.co/shortcut/13988/ + + +
+ +
+ +
+ + Is it possible to send files or text directly from the context or share menu? + + +Yes, it finally is! +* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) +* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) +* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android) + + +
+ +
+ +
+ + Is it possible to send files or text directly via CLI? + + +Yes, it is! + +* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) + + +
+ +
+ +
+ + Are there any Third-Party Apps? + + +Here's a list of some third-party apps compatible with MoreLoad: + +1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android) +2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon) +3. Feel free to make one :) + +
+ +
+ +
+ + What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? + + +It uses a WebRTC peer to peer connection. WebRTC needs a Signaling Server that is only used to establish a connection. The server is not involved in the file transfer. + +If devices are on the same network, none of your files are ever sent to any server. + +If your devices are paired and behind a NAT, the MoreLoad TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC. + +If you host your own instance and want to support devices that do not support WebRTC, you can [start the MoreLoad instance with an activated Websocket fallback](https://github.com/MoreLoad/MoreLoad/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn). + +
+ +
+ +
+ + What about privacy? Will files be saved on third-party-servers? + + +Files are sent directly between peers. MoreLoad doesn't even use a database. If you are curious, have a look [at the Server](https://github.com/moreload/moreload/blob/master/index.js). +WebRTC encrypts the files on transit. + +If devices are on the same network, none of your files are ever sent to any server. + +If your devices are paired and behind a NAT, the MoreLoad TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC. + +
+ +
+ +
+ + What about security? Are my files encrypted while being sent between the computers? + + +Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection. + + +
+ +
+ +
+ + Transferring many files with paired devices takes too long + + +Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases. + +You can open a hotspot on one of your devices to bridge the connection which omits the need of the TURN server. + +- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11) +- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) +- [Library to open a hotspot on Linux](https://github.com/lakinduakash/linux-wifi-hotspot) + +You can also use mobile hotspots on phones to do that. +Then, all data should be sent directly between devices and your data plan should not be charged. + + +
+ +
+ +
+ + Why don't you implement feature xyz? + + +Snapdrop and MoreLoad are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer. +We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity. + +If you want to learn more about simplicity you can read *Insanely Simple: The Obsession that Drives Apple's Success* or *Thinking, Fast and Slow*. + + +
+ +
+ +
+ + Snapdrop and MoreLoad are awesome! How can I support them? + + +* [File bugs, give feedback, submit suggestions](https://github.com/moreload/moreload/issues) +* Share MoreLoad on social media. +* Fix bugs and make a pull request. +* Do security analysis and suggestions +* To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop) + + +
+ +
+ +
+ + How does it work? + + +[See here for Information about the Technical Implementation](/docs/technical-documentation.md) + +
+ +
+ +[< Back](/README.md) diff --git a/docs/host-your-own.md b/docs/host-your-own.md new file mode 100644 index 0000000..ef4eb97 --- /dev/null +++ b/docs/host-your-own.md @@ -0,0 +1,490 @@ +# Deployment Notes +The easiest way to get MoreLoad up and running is by using Docker. + +> TURN server for Internet Transfer +> +> Beware that you have to host your own TURN server in order to enable transfers between different networks. +> +> You can follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) or deploy it via docker-compose (Step 5). + +## Deployment with Docker + +### Docker Image from Docker Hub + +```bash +docker run -d --restart=unless-stopped --name=moreload -p 127.0.0.1:3000:3000 lscr.io/linuxserver/moreload +``` + +> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. + +#### Options / Flags +Set options by using the following flags in the `docker run` command: + +##### Port +```bash +-p 127.0.0.1:8080:3000 +``` +> Specify the port used by the docker image +> - 3000 -> `-p 127.0.0.1:3000:3000` +> - 8080 -> `-p 127.0.0.1:8080:3000` +##### Rate limiting requests +```bash +-e RATE_LIMIT=true +``` +> Limits clients to 1000 requests per 5 min + +##### IPv6 Localization +```bash +-e IPV6_LOCALIZE=4 +``` +> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments of the client IPv6 address to be evaluated as the peer's IP. This can be especially useful when using Cloudflare as a proxy. +> +> The flag must be set to an **integer** between `1` and `7`. The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) to match the client IP against. The most common value would be `4`, which will group peers within the same `/64` subnet. + +##### Websocket Fallback (for VPN) +```bash +-e WS_FALLBACK=true +``` +> Provides MoreLoad to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. +> +> This is not used on the official https://moreload.com, but you can activate it on your self-hosted instance. +> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). +> +> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer! +> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. +> Additionally, beware that all traffic using this fallback debits the servers data plan. + +##### Specify STUN/TURN Servers +```bash +-e RTC_CONFIG="rtc_config.json" +``` + +> Specify the STUN/TURN servers MoreLoad clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> You can use `moreload/rtc_config_example.json` as a starting point. +> +> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ +> +> Default configuration: +> ```json +> { +> "sdpSemantics": "unified-plan", +> "iceServers": [ +> { +> "urls": "stun:stun.l.google.com:19302" +> } +> ] +> } +> ``` + +##### Debug Mode +```bash +-e DEBUG_MODE="true" +``` + +> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) +> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> +> If this flag is set to `"true"` each peer that connects to the MoreLoad server will produce a log to STDOUT like this: +> ``` +> ----DEBUGGING-PEER-IP-START---- +> remoteAddress: ::ffff:172.17.0.1 +> x-forwarded-for: 19.117.63.126 +> cf-connecting-ip: undefined +> MoreLoad uses: 19.117.63.126 +> IP is private: false +> if IP is private, '127.0.0.1' is used instead +> ----DEBUGGING-PEER-IP-END---- +> ``` +> If the IP MoreLoad uses is the public IP of your device everything is correctly setup. +>To find out your devices public IP visit https://www.whatismyip.com/. +> +> To preserve your clients' privacy, **never use this flag in production!** + + +
+ +### Docker Image from GHCR +```bash +docker run -d --restart=unless-stopped --name=moreload -p 127.0.0.1:3000:3000 ghcr.io/moreload/moreload npm run start:prod +``` +> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> +> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) + +> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage) + +### Docker Image self-built +#### Build the image +```bash +docker build --pull . -f Dockerfile -t moreload +``` +> A GitHub action is set up to do this step automatically. +> +> `--pull` ensures always the latest node image is used. + +#### Run the image +```bash +docker run -d --restart=unless-stopped --name=moreload -p 127.0.0.1:3000:3000 -it moreload npm run start:prod +``` +> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> +> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) + +> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage) + +
+ +## Deployment with Docker Compose +Here's an example docker-compose file: + +```yaml +version: "2" +services: + moreload: + image: lscr.io/linuxserver/moreload:latest + container_name: moreload + restart: unless-stopped + environment: + - PUID=1000 # UID to run the application as + - PGID=1000 # GID to run the application as + - WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client. + - RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min. + - TZ=Etc/UTC # Time Zone + ports: + - 127.0.0.1:3000:3000 # Web UI +``` + +Run the compose file with `docker compose up -d`. + +> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. + +
+ +## Deployment with node + +```bash +git clone https://github.com/MoreLoad/MoreLoad.git && cd MoreLoad +``` + +Install all dependencies with NPM: + +```bash +npm install +``` + +Start the server with: + +```bash +node index.js +``` +or +```bash +npm start +``` + +> Remember to check your IP Address using your OS command to see where you can access the server. + +> By default, the node server listens on port 3000. + +
+ +### Environment variables +#### Port +On Unix based systems +```bash +PORT=3010 npm start +``` +On Windows +```bash +$env:PORT=3010; npm start +``` +> Specify the port MoreLoad is running on. (Default: 3000) + +#### IPv6 Localization +```bash +IPV6_LOCALIZE=4 +``` +> Truncate a portion of the client IPv6 address to make peers more discoverable. See [Options/Flags](#options--flags) above. + +#### Specify STUN/TURN Server +On Unix based systems +```bash +RTC_CONFIG="rtc_config.json" npm start +``` +On Windows +```bash +$env:RTC_CONFIG="rtc_config.json"; npm start +``` +> Specify the STUN/TURN servers MoreLoad clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> You can use `moreload/rtc_config_example.json` as a starting point. +> +> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ +> +> Default configuration: +> ```json +> { +> "sdpSemantics": "unified-plan", +> "iceServers": [ +> { +> "urls": "stun:stun.l.google.com:19302" +> } +> ] +> } +> ``` + +#### Debug Mode +On Unix based systems +```bash +DEBUG_MODE="true" npm start +``` +On Windows +```bash +$env:DEBUG_MODE="true"; npm start +``` + +> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) +> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> +> If this flag is set to `"true"` each peer that connects to the MoreLoad server will produce a log to STDOUT like this: +> ``` +> ----DEBUGGING-PEER-IP-START---- +> remoteAddress: ::ffff:172.17.0.1 +> x-forwarded-for: 19.117.63.126 +> cf-connecting-ip: undefined +> MoreLoad uses: 19.117.63.126 +> IP is private: false +> if IP is private, '127.0.0.1' is used instead +> ----DEBUGGING-PEER-IP-END---- +> ``` +> If the IP MoreLoad uses is the public IP of your device everything is correctly setup. +>To find out your devices public IP visit https://www.whatismyip.com/. +> +> To preserve your clients' privacy, **never use this flag in production!** + + +### Options / Flags +#### Local Run +```bash +npm start -- --localhost-only +``` +> Only allow connections from localhost. +> +> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> +> Use this when deploying MoreLoad with node to prevent bypassing the proxy by reaching the docker container directly. + +#### Automatic restart on error +```bash +npm start -- --auto-restart +``` +> Restarts server automatically on error + +
+ +#### Rate limiting requests +```bash +npm start -- --rate-limit +``` +> Limits clients to 1000 requests per 5 min + +
+ +#### Websocket Fallback (for VPN) +```bash +npm start -- --include-ws-fallback +``` +> Provides MoreLoad to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. +> +> This is not used on the official https://moreload.com, but you can activate it on your self-hosted instance. +> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). +> +> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer! +> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. +> Additionally, beware that all traffic using this fallback debits the servers data plan. + +
+ +#### Production (autostart and rate-limit) +```bash +npm run start:prod +``` + +#### Production (autostart, rate-limit, localhost-only and websocket fallback for VPN) +```bash +npm run start:prod -- --localhost-only --include-ws-fallback +``` +> To prevent connections to the node server from bypassing the proxy server you should always use "--localhost-only" on production. + +## HTTP-Server +When running MoreLoad, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible. + +To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode). + +### Using nginx +#### Allow http and https requests +``` +server { + listen 80; + + expires epoch; + + location / { + proxy_connect_timeout 300; + proxy_pass http://127.0.0.1:3000; + proxy_set_header Connection "upgrade"; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Forwarded-for $remote_addr; + } +} + +server { + listen 443 ssl http2; + ssl_certificate /etc/ssl/certs/moreload-dev.crt; + ssl_certificate_key /etc/ssl/certs/moreload-dev.key; + + expires epoch; + + location / { + proxy_connect_timeout 300; + proxy_pass http://127.0.0.1:3000; + proxy_set_header Connection "upgrade"; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Forwarded-for $remote_addr; + } +} +``` + +#### Automatic http to https redirect: +``` +server { + listen 80; + + expires epoch; + + location / { + return 301 https://$host:3000$request_uri; + } +} + +server { + listen 443 ssl http2; + ssl_certificate /etc/ssl/certs/moreload-dev.crt; + ssl_certificate_key /etc/ssl/certs/moreload-dev.key; + + expires epoch; + + location / { + proxy_connect_timeout 300; + proxy_pass http://127.0.0.1:3000; + proxy_set_header Connection "upgrade"; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Forwarded-for $remote_addr; + } +} +``` + +### Using Apache +install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel` +```bash +a2enmod proxy +``` +```bash +a2enmod proxy_http +``` +```bash +a2enmod proxy_wstunnel +``` + +
+ +Create a new configuration file under `/etc/apache2/sites-available` (on debian) + +**moreload.conf** +#### Allow http and https requests +```apacheconf + + ProxyPass / http://127.0.0.1:3000/ + RewriteEngine on + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + RewriteRule ^/?(.*) "ws://127.0.0.1:3000/$1" [P,L] + + + ProxyPass / https://127.0.0.1:3000/ + RewriteEngine on + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L] + +``` +#### Automatic http to https redirect: +```apacheconf + + Redirect permanent / https://127.0.0.1:3000/ + + + ProxyPass / https://127.0.0.1:3000/ + RewriteEngine on + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L] + +``` +Activate the new virtual host and reload apache: +```bash +a2ensite moreload +``` +```bash +service apache2 reload +``` + +# Local Development +## Install +All files needed for developing are available on the branch `dev`. + +First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/) + +Then, clone the repository and run docker-compose: +```bash + git clone https://github.com/MoreLoad/MoreLoad.git + + cd MoreLoad + + git checkout dev + + docker-compose up -d +``` +Now point your browser to `http://localhost:8080`. + +- To restart the containers run `docker-compose restart`. +- To stop the containers run `docker-compose stop`. +- To debug the NodeJS server run `docker logs moreload_node_1`. + + +
+ +## Testing PWA related features +PWAs require that the app is served under a correctly set up and trusted TLS endpoint. + +The nginx container creates a CA certificate and a website certificate for you. To correctly set the common name of the certificate, you need to change the FQDN environment variable in `docker/fqdn.env` to the fully qualified domain name of your workstation. + +If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://:8080/ca.crt`. Install that certificate to the trust store of your operating system. +- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. +- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL. +- Firefox uses its own trust store. To install the CA, point Firefox at `http://:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK. +- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data). + +Please note that the certificates (CA and webserver cert) expire after a day. +Also, whenever you restart the nginx docker, container new certificates are created. + +The site is served on `https://:8443`. + +[< Back](/README.md) diff --git a/docs/how-to.md b/docs/how-to.md new file mode 100644 index 0000000..afccd6c --- /dev/null +++ b/docs/how-to.md @@ -0,0 +1,82 @@ +# How-To +## Send files directly from context menu on Windows +### Registering to open files with MoreLoad +The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files) is implemented + +This is still experimental and must be enabled via a flag **before** the PWA is installed to Windows. +1. [Enabled feature in Edge](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files#enable-the-file-handling-api) +2. Install MoreLoad by visiting https://moreload.com/ with the Edge browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-). +3. You are done! You can now send most files one at a time via MoreLoad: + + _context menu > Open with > MoreLoad_ + +[//]: # (Todo: add screenshots) + +### Sending multiple files to MoreLoad +Outstandingly, it is also possible to send multiple files to MoreLoad via the context menu by adding MoreLoad to the `Send to` menu: +1. [Register MoreLoad as file handler](#registering-to-open-files-with-moreload) +2. Hit Windows Key+R, type: `shell:programs` and hit Enter. +3. Copy the MoreLoad shortcut from the directory +4. Hit Windows Key+R, type: `shell:sendto` and hit Enter. +5. Paste the copied shortcut into the directory +6. You are done! You can now send multiple files (but no directories) directly via MoreLoad: + + _context menu > Send to > MoreLoad_ + +[//]: # (Todo: add screenshots) + +## Send directly from share menu on iOS +I created an iOS shortcut to send images, files, folder, URLs or text directly from the share-menu +https://routinehub.co/shortcut/13990/ + +[//]: # (Todo: add doku with screenshots) + + +## Send directly from share menu on Android +The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented. + +When the PWA is installed, it will register itself to the share-menu of the device automatically. + + +## Send directly via command-line interface +Send files or text with MoreLoad via command-line interface. + +This opens MoreLoad in the default browser where you can choose the receiver. + +### Usage +```bash +$ moreload -h +Current domain: https://moreload.com/ + +Usage: +Open MoreLoad: moreload +Send files: moreload file/directory +Send text: moreload -t "text" +Specify domain: moreload -d "https://moreload.com/" +Show this help text: moreload (-h|--help) +``` + +On Windows Command Prompt you need to use bash: `bash moreload -h` + + +### Setup +Download the bash file: [moreload-cli/moreload](/moreload-cli/moreload). + +#### Linux +1. Put file in a preferred folder e.g. `/usr/local/bin` +2. Make sure the bash file is executable. Otherwise, use `chmod +x moreload` +3. Add absolute path of the folder to PATH variable to make `moreload` available globally by executing + `export PATH=$PATH:/opt/moreload-cli` + +#### Mac +1. add bash file to `/usr/local/bin` + +#### Windows +1. Put file in a preferred folder e.g. `C:\Users\Public\moreload-cli` +2. Search for and open `Edit environment variables for your account` +3. Click `Environment Variables...` +4. Under *System Variables* select `Path` and click *Edit...* +5. Click *New*, insert the preferred folder (`C:\Users\Public\moreload-cli`), click *OK* until all windows are closed +6. Reopen Command prompt window + +[< Back](/README.md) diff --git a/docs/moreload_screenshot_desktop.png b/docs/moreload_screenshot_desktop.png new file mode 100644 index 0000000..626153a Binary files /dev/null and b/docs/moreload_screenshot_desktop.png differ diff --git a/docs/moreload_screenshot_mobile.gif b/docs/moreload_screenshot_mobile.gif new file mode 100644 index 0000000..1813720 Binary files /dev/null and b/docs/moreload_screenshot_mobile.gif differ diff --git a/docs/pwa-install.png b/docs/pwa-install.png new file mode 100644 index 0000000..4fe8cea Binary files /dev/null and b/docs/pwa-install.png differ diff --git a/docs/technical-documentation.md b/docs/technical-documentation.md new file mode 100644 index 0000000..db5bf34 --- /dev/null +++ b/docs/technical-documentation.md @@ -0,0 +1,50 @@ +# Technical Documentation +## Encryption, WebRTC, STUN and TURN + +Encryption is mandatory for WebRTC connections and completely done by the browser itself. + +When the peers are first connecting, a channel is created by exchanging their signaling information. +This signaling information includes some sort of public key and is specific to the clients ip address. +That is what the STUN Server is used for: it simply returns your public IP address as you only know your local ip address +if behind a NAT (router). + +The transfer of the signaling information is done by the MoreLoad / Snapdrop server using secure websockets. +After that the channel itself is completely peer-2-peer and all information can only be decrypted by the receiver. +When the two peers are on the same network or when they are not behind any NAT system (which they are always for classic +Snapdrop and for not paired users on MoreLoad) the files are send directly peer to peer. + +When a user is behind a NAT (behind a router) the contents are channeled through a TURN server. +But again, the contents send via the channel can only be decrypted by the receiver. So a rogue TURN server could only +see that there is a connection, but not what is sent. Obviously, connections which are channeled through a TURN server +are not as fast as peer to peer. + +The selection whether a TURN server is needed or not is also done automatically by the browser. +It simply iterated through the configured RTC iceServers and checks what works. Only if the STUN server is not sufficient, +the TURN server is used. + +![img](https://www.wowza.com/wp-content/uploads/WeRTC-Encryption-Diagrams-01.jpg) +_Diagram created by wowza.com_ + +Good thing: if your device has an IPv6 address it is uniquely reachable by that address. As I understand it, when both devices are using IPv6 addresses there is no need for a TURN server in any scenario. + +To learn more take a look at https://www.wowza.com/blog/webrtc-encryption-and-security which gives a good insight into stun, turn and webrtc + + +## Device Pairing + +The pairing functionality uses the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). + +It works by creating long secrets that are served by the server to the initiating and requesting pair peer, +when the inserted key is correct. These long secrets are then saved to an indexedDB database in the browser. +IndexedDB is somewhat the successor of localStorage as saved data is shared between all tabs. +It goes one step further by making the data persistent and available offline if implemented to a PWA. + +All secrets a client has saved to its database are send to the MoreLoad server. Peers with a common secret are discoverable +to each other analog to peers with the same ip-address are discoverable to each other. + +What I really like about this approach, and the reason why I implemented it, is that devices on the same network are always +visible regardless whether any devices are paired or not. The main user flow is never obstructed. Paired devices are simply +shown additionally. This makes it in my idea better than the idea of using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214). + + +[< Back](/README.md) diff --git a/index.js b/index.js new file mode 100644 index 0000000..e624b97 --- /dev/null +++ b/index.js @@ -0,0 +1,729 @@ +const process = require('process') +const crypto = require('crypto') +const {spawn} = require('child_process') +const WebSocket = require('ws'); +const fs = require('fs'); +const parser = require('ua-parser-js'); +const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator'); +const express = require('express'); +const RateLimit = require('express-rate-limit'); +const http = require('http'); + +// Handle SIGINT +process.on('SIGINT', () => { + console.info("SIGINT Received, exiting...") + process.exit(0) +}) + +// Handle SIGTERM +process.on('SIGTERM', () => { + console.info("SIGTERM Received, exiting...") + process.exit(0) +}) + +// Handle APP ERRORS +process.on('uncaughtException', (error, origin) => { + console.log('----- Uncaught exception -----') + console.log(error) + console.log('----- Exception origin -----') + console.log(origin) +}) +process.on('unhandledRejection', (reason, promise) => { + console.log('----- Unhandled Rejection at -----') + console.log(promise) + console.log('----- Reason -----') + console.log(reason) +}) + +if (process.argv.includes('--auto-restart')) { + process.on( + 'uncaughtException', + () => { + process.once( + 'exit', + () => spawn( + process.argv.shift(), + process.argv, + { + cwd: process.cwd(), + detached: true, + stdio: 'inherit' + } + ) + ); + process.exit(); + } + ); +} + +const rtcConfig = process.env.RTC_CONFIG + ? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8')) + : { + "sdpSemantics": "unified-plan", + "iceServers": [ + { + "urls": "stun:stun.l.google.com:19302" + } + ] + }; + +const app = express(); + +if (process.argv.includes('--rate-limit')) { + const limiter = RateLimit({ + windowMs: 5 * 60 * 1000, // 5 minutes + max: 1000, // Limit each IP to 1000 requests per `window` (here, per 5 minutes) + message: 'Too many requests from this IP Address, please try again after 5 minutes.', + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + }) + + app.use(limiter); + // ensure correct client ip and not the ip of the reverse proxy is used for rate limiting on render.com + // see https://github.com/express-rate-limit/express-rate-limit#troubleshooting-proxy-issues + app.set('trust proxy', 5); +} + +if (process.argv.includes('--include-ws-fallback')) { + app.use(express.static('public_included_ws_fallback')); +} else { + app.use(express.static('public')); +} + +const debugMode = process.env.DEBUG_MODE === "true"; + +if (debugMode) { + console.log("DEBUG_MODE is active. To protect privacy, do not use in production.") +} + +let ipv6_lcl; +if (process.env.IPV6_LOCALIZE) { + ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE); + if (!ipv6_lcl || !(0 < ipv6_lcl && ipv6_lcl < 8)) { + console.error("IPV6_LOCALIZE must be an integer between 1 and 7"); + return; + } + + console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl === 1 ? "segment" : "segments"); +} + +app.use(function(req, res) { + res.redirect('/'); +}); + +app.get('/', (req, res) => { + res.sendFile('index.html'); +}); + +const server = http.createServer(app); +const port = process.env.PORT || 3000; + +if (process.argv.includes('--localhost-only')) { + server.listen(port, '127.0.0.1'); +} else { + server.listen(port); +} + +class MoreLoadServer { + + constructor() { + this._wss = new WebSocket.Server({ server }); + this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); + + this._rooms = {}; + this._roomSecrets = {}; + + console.log('MoreLoad is running on port', port); + } + + _onConnection(peer) { + peer.socket.on('message', message => this._onMessage(peer, message)); + peer.socket.onerror = e => console.error(e); + this._keepAlive(peer); + this._send(peer, { + type: 'rtc-config', + config: rtcConfig + }); + + // send displayName + this._send(peer, { + type: 'display-name', + message: { + displayName: peer.name.displayName, + deviceName: peer.name.deviceName, + peerId: peer.id, + peerIdHash: hasher.hashCodeSalted(peer.id) + } + }); + } + + _onMessage(sender, message) { + // Try to parse message + try { + message = JSON.parse(message); + } catch (e) { + return; // TODO: handle malformed JSON + } + + switch (message.type) { + case 'disconnect': + this._onDisconnect(sender); + break; + case 'pong': + sender.lastBeat = Date.now(); + break; + case 'join-ip-room': + this._joinRoom(sender); + break; + case 'room-secrets': + this._onRoomSecrets(sender, message); + break; + case 'room-secrets-deleted': + this._onRoomSecretsDeleted(sender, message); + break; + case 'pair-device-initiate': + this._onPairDeviceInitiate(sender); + break; + case 'pair-device-join': + this._onPairDeviceJoin(sender, message); + break; + case 'pair-device-cancel': + this._onPairDeviceCancel(sender); + break; + case 'regenerate-room-secret': + this._onRegenerateRoomSecret(sender, message); + break + case 'resend-peers': + this._notifyPeers(sender); + break; + case 'signal': + default: + this._signalAndRelay(sender, message); + } + } + + _signalAndRelay(sender, message) { + const room = message.roomType === 'ip' ? sender.ip : message.roomSecret; + + // relay message to recipient + if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) { + const recipient = this._rooms[room][message.to]; + delete message.to; + // add sender + message.sender = { + id: sender.id, + rtcSupported: sender.rtcSupported + }; + this._send(recipient, message); + } + } + + _onDisconnect(sender) { + this._leaveRoom(sender, 'ip', '', true); + this._leaveAllSecretRooms(sender, true); + this._removeRoomKey(sender.roomKey); + sender.roomKey = null; + } + + _onRoomSecrets(sender, message) { + if (!message.roomSecrets) return; + + const roomSecrets = message.roomSecrets.filter(roomSecret => { + return /^[\x00-\x7F]{64,256}$/.test(roomSecret); + }) + + if (!roomSecrets) return; + + this._joinSecretRooms(sender, roomSecrets); + } + + _onRoomSecretsDeleted(sender, message) { + for (let i = 0; i= 10) { + this._send(sender, { type: 'pair-device-join-key-rate-limit' }); + return; + } + sender.roomKeyRate += 1; + setTimeout(_ => sender.roomKeyRate -= 1, 10000); + + if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) { + this._send(sender, { type: 'pair-device-join-key-invalid' }); + return; + } + + const roomSecret = this._roomSecrets[message.roomKey].roomSecret; + const creator = this._roomSecrets[message.roomKey].creator; + this._removeRoomKey(message.roomKey); + this._send(sender, { + type: 'pair-device-joined', + roomSecret: roomSecret, + peerId: creator.id + }); + this._send(creator, { + type: 'pair-device-joined', + roomSecret: roomSecret, + peerId: sender.id + }); + this._joinRoom(sender, 'secret', roomSecret); + this._removeRoomKey(sender.roomKey); + } + + _onPairDeviceCancel(sender) { + const roomKey = sender.roomKey + + if (!roomKey) return; + + this._removeRoomKey(roomKey); + this._send(sender, { + type: 'pair-device-canceled', + roomKey: roomKey, + }); + } + + _onRegenerateRoomSecret(sender, message) { + const oldRoomSecret = message.roomSecret; + const newRoomSecret = randomizer.getRandomString(256); + + // notify all other peers + for (const peerId in this._rooms[oldRoomSecret]) { + const peer = this._rooms[oldRoomSecret][peerId]; + this._send(peer, { + type: 'room-secret-regenerated', + oldRoomSecret: oldRoomSecret, + newRoomSecret: newRoomSecret, + }); + peer.removeRoomSecret(oldRoomSecret); + } + delete this._rooms[oldRoomSecret]; + } + + _createRoomKey(creator, roomSecret) { + let roomKey; + do { + // get randomInt until keyRoom not occupied + roomKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s + } while (roomKey in this._roomSecrets) + + this._roomSecrets[roomKey] = { + roomSecret: roomSecret, + creator: creator + } + + return roomKey; + } + + _removeRoomKey(roomKey) { + if (roomKey in this._roomSecrets) { + this._roomSecrets[roomKey].creator.roomKey = null + delete this._roomSecrets[roomKey]; + } + } + + _joinRoom(peer, roomType = 'ip', roomSecret = '') { + const room = roomType === 'ip' ? peer.ip : roomSecret; + + if (this._rooms[room] && this._rooms[room][peer.id]) { + this._leaveRoom(peer, roomType, roomSecret); + } + + // if room doesn't exist, create it + if (!this._rooms[room]) { + this._rooms[room] = {}; + } + + this._notifyPeers(peer, roomType, roomSecret); + + // add peer to room + this._rooms[room][peer.id] = peer; + // add secret to peer + if (roomType === 'secret') { + peer.addRoomSecret(roomSecret); + } + } + + _leaveRoom(peer, roomType = 'ip', roomSecret = '', disconnect = false) { + const room = roomType === 'ip' ? peer.ip : roomSecret; + + if (!this._rooms[room] || !this._rooms[room][peer.id]) return; + this._cancelKeepAlive(this._rooms[room][peer.id]); + + // delete the peer + delete this._rooms[room][peer.id]; + + if (roomType === 'ip') { + peer.socket.terminate(); + } + + //if room is empty, delete the room + if (!Object.keys(this._rooms[room]).length) { + delete this._rooms[room]; + } else { + // notify all other peers + for (const otherPeerId in this._rooms[room]) { + const otherPeer = this._rooms[room][otherPeerId]; + this._send(otherPeer, { + type: 'peer-left', + peerId: peer.id, + roomType: roomType, + roomSecret: roomSecret, + disconnect: disconnect + }); + } + } + //remove secret from peer + if (roomType === 'secret') { + peer.removeRoomSecret(roomSecret); + } + } + + _notifyPeers(peer, roomType = 'ip', roomSecret = '') { + const room = roomType === 'ip' ? peer.ip : roomSecret; + if (!this._rooms[room]) return; + + // notify all other peers + for (const otherPeerId in this._rooms[room]) { + if (otherPeerId === peer.id) continue; + const otherPeer = this._rooms[room][otherPeerId]; + this._send(otherPeer, { + type: 'peer-joined', + peer: peer.getInfo(), + roomType: roomType, + roomSecret: roomSecret + }); + } + + // notify peer about the other peers + const otherPeers = []; + for (const otherPeerId in this._rooms[room]) { + if (otherPeerId === peer.id) continue; + otherPeers.push(this._rooms[room][otherPeerId].getInfo()); + } + + this._send(peer, { + type: 'peers', + peers: otherPeers, + roomType: roomType, + roomSecret: roomSecret + }); + } + + _joinSecretRooms(peer, roomSecrets) { + for (let i=0; i 2 * timeout) { + this._leaveRoom(peer); + this._leaveAllSecretRooms(peer); + return; + } + + this._send(peer, { type: 'ping' }); + + peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); + } + + _cancelKeepAlive(peer) { + if (peer && peer.timerId) { + clearTimeout(peer.timerId); + } + } +} + + + +class Peer { + + constructor(socket, request) { + // set socket + this.socket = socket; + + // set remote ip + this._setIP(request); + + // set peer id + this._setPeerId(request); + + // is WebRTC supported ? + this.rtcSupported = request.url.indexOf('webrtc') > -1; + + // set name + this._setName(request); + + // for keepalive + this.timerId = 0; + this.lastBeat = Date.now(); + + this.roomSecrets = []; + this.roomKey = null; + this.roomKeyRate = 0; + } + + _setIP(request) { + if (request.headers['cf-connecting-ip']) { + this.ip = request.headers['cf-connecting-ip'].split(/\s*,\s*/)[0]; + } else if (request.headers['x-forwarded-for']) { + this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; + } else { + this.ip = request.connection.remoteAddress; + } + + // remove the prefix used for IPv4-translated addresses + if (this.ip.substring(0,7) === "::ffff:") + this.ip = this.ip.substring(7); + + let ipv6_was_localized = false; + if (ipv6_lcl && this.ip.includes(':')) { + this.ip = this.ip.split(':',ipv6_lcl).join(':'); + ipv6_was_localized = true; + } + + if (debugMode) { + console.debug("----DEBUGGING-PEER-IP-START----"); + console.debug("remoteAddress:", request.connection.remoteAddress); + console.debug("x-forwarded-for:", request.headers['x-forwarded-for']); + console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']); + if (ipv6_was_localized) + console.debug("IPv6 client IP was localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment"); + console.debug("MoreLoad uses:", this.ip); + console.debug("IP is private:", this.ipIsPrivate(this.ip)); + console.debug("if IP is private, '127.0.0.1' is used instead"); + console.debug("----DEBUGGING-PEER-IP-END----"); + } + + // IPv4 and IPv6 use different values to refer to localhost + // put all peers on the same network as the server into the same room as well + if (this.ip === '::1' || this.ipIsPrivate(this.ip)) { + this.ip = '127.0.0.1'; + } + } + + ipIsPrivate(ip) { + // if ip is IPv4 + if (!ip.includes(":")) { + // 10.0.0.0 - 10.255.255.255 || 172.16.0.0 - 172.31.255.255 || 192.168.0.0 - 192.168.255.255 + return /^(10)\.(.*)\.(.*)\.(.*)$/.test(ip) || /^(172)\.(1[6-9]|2[0-9]|3[0-1])\.(.*)\.(.*)$/.test(ip) || /^(192)\.(168)\.(.*)\.(.*)$/.test(ip) + } + + // else: ip is IPv6 + const firstWord = ip.split(":").find(el => !!el); //get first not empty word + + // The original IPv6 Site Local addresses (fec0::/10) are deprecated. Range: fec0 - feff + if (/^fe[c-f][0-f]$/.test(firstWord)) + return true; + + // These days Unique Local Addresses (ULA) are used in place of Site Local. + // Range: fc00 - fcff + else if (/^fc[0-f]{2}$/.test(firstWord)) + return true; + + // Range: fd00 - fcff + else if (/^fd[0-f]{2}$/.test(firstWord)) + return true; + + // Link local addresses (prefixed with fe80) are not routable + else if (firstWord === "fe80") + return true; + + // Discard Prefix + else if (firstWord === "100") + return true; + + // Any other IP address is not Unique Local Address (ULA) + return false; + } + + _setPeerId(request) { + const searchParams = new URL(request.url, "http://server").searchParams; + let peerId = searchParams.get("peer_id"); + let peerIdHash = searchParams.get("peer_id_hash"); + if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) { + this.id = peerId; + } else { + this.id = crypto.randomUUID(); + } + } + + toString() { + return `` + } + + _setName(req) { + let ua = parser(req.headers['user-agent']); + + + let deviceName = ''; + + if (ua.os && ua.os.name) { + deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' '; + } + + if (ua.device.model) { + deviceName += ua.device.model; + } else { + deviceName += ua.browser.name; + } + + if(!deviceName) + deviceName = 'Unknown Device'; + + const displayName = uniqueNamesGenerator({ + length: 2, + separator: ' ', + dictionaries: [colors, animals], + style: 'capital', + seed: cyrb53(this.id) + }) + + this.name = { + model: ua.device.model, + os: ua.os.name, + browser: ua.browser.name, + type: ua.device.type, + deviceName, + displayName + }; + } + + getInfo() { + return { + id: this.id, + name: this.name, + rtcSupported: this.rtcSupported + } + } + + static isValidUuid(uuid) { + return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid); + } + + isPeerIdHashValid(peerId, peerIdHash) { + return peerIdHash === hasher.hashCodeSalted(peerId); + } + + addRoomSecret(roomSecret) { + if (!(roomSecret in this.roomSecrets)) { + this.roomSecrets.push(roomSecret); + } + } + + removeRoomSecret(roomSecret) { + if (roomSecret in this.roomSecrets) { + delete this.roomSecrets[roomSecret]; + } + } +} + +const hasher = (() => { + let password; + return { + hashCodeSalted(salt) { + if (!password) { + // password is created on first call. + password = randomizer.getRandomString(128); + } + + return crypto.createHash("sha3-512") + .update(password) + .update(crypto.createHash("sha3-512").update(salt, "utf8").digest("hex")) + .digest("hex"); + } + } +})() + +const randomizer = (() => { + return { + getRandomString(length) { + let string = ""; + while (string.length < length) { + let arr = new Uint16Array(length); + crypto.webcrypto.getRandomValues(arr); + arr = Array.apply([], arr); /* turn into non-typed array */ + arr = arr.map(function (r) { + return r % 128 + }) + arr = arr.filter(function (r) { + /* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */ + return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122; + }); + string += String.fromCharCode.apply(String, arr); + } + return string.substring(0, length) + } + } +})() + +/* + cyrb53 (c) 2018 bryc (github.com/bryc) + A fast and simple hash function with decent collision resistance. + Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. + Public domain. Attribution appreciated. +*/ +const cyrb53 = function(str, seed = 0) { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909); + h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1>>>0); +}; + +new MoreLoadServer(); diff --git a/moreload-cli/moreload b/moreload-cli/moreload new file mode 100644 index 0000000..3f3205b --- /dev/null +++ b/moreload-cli/moreload @@ -0,0 +1,207 @@ +#!/bin/bash +set -e + +############################################################ +# Help # +############################################################ +help() +{ + # Display Help + echo "Send files or text with MoreLoad via command-line interface." + echo "Current domain: ${DOMAIN}" + echo + echo "Usage:" + echo -e "Open MoreLoad:\t\t$(basename "$0")" + echo -e "Send files:\t\t$(basename "$0") file/directory" + echo -e "Send text:\t\t$(basename "$0") -t \"text\"" + echo -e "Specify domain:\t\t$(basename "$0") -d \"https://moreload.com/\"" + echo -e "Show this help text:\t$(basename "$0") (-h|--help)" +} + +openMoreLoad() +{ + url="$DOMAIN" + if [[ -n $params ]];then + url="${url}?${params}" + fi + if [[ -n $hash ]];then + url="${url}#${hash}" + fi + + echo "MoreLoad is opening at $DOMAIN" + if [[ $OS == "Windows" ]];then + start "$url" + elif [[ $OS == "Mac" ]];then + open "$url" + elif [[ $OS == "WSL" || $OS == "WSL2" ]];then + powershell.exe /c "Start-Process ${url}" + else + xdg-open "$url" + fi + exit +} + +setOs() +{ + unameOut=$(uname -a) + case "${unameOut}" in + *Microsoft*) OS="WSL";; #must be first since Windows subsystem for linux will have Linux in the name too + *microsoft*) OS="WSL2";; #WARNING: My v2 uses ubuntu 20.4 at the moment slightly different name may not always work + Linux*) OS="Linux";; + Darwin*) OS="Mac";; + CYGWIN*) OS="Cygwin";; + MINGW*) OS="Windows";; + *Msys) OS="Windows";; + *) OS="UNKNOWN:${unameOut}" + esac +} + +specifyDomain() +{ + [[ ! $1 = http* ]] || [[ ! $1 = */ ]] && echo "Incorrect format. Specify domain like https://moreload.com/" && exit + echo "DOMAIN=${1}" > "$CONFIGPATH" + echo -e "Domain is now set to:\n$1\n" +} + +sendText() +{ + params="base64text=hash" + hash=$(echo -n "${OPTARG}" | base64) + + if [[ $(echo -n "$hash" | wc -m) -gt 32600 ]];then + params="base64text=paste" + if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then + echo -n "$hash" | clip.exe + elif [[ $OS == "Mac" ]];then + echo -n "$hash" | pbcopy + else + (echo -n "$hash" | xclip) || echo "You need to install xclip for sending bigger files from cli" + fi + hash= + fi + + openMoreLoad + exit +} + +sendFiles() +{ + params="base64zip=hash" + if [[ $1 == */ ]]; then + path="${1::-1}" + else + path=$1 + fi + zipPath="${path}_moreload.zip" + zipPath=${zipPath// /_} + + [[ -a "$zipPath" ]] && echo "Cannot overwrite $zipPath. Please remove first." && exit + + if [[ -d $path ]]; then + zipPathTemp="temp_${zipPath}" + [[ -a "$zipPathTemp" ]] && echo "Cannot overwrite $zipPathTemp. Please remove first." && exit + echo "Processing directory..." + + # Create zip files temporarily to send directory + zip -q -b /tmp/ -r "$zipPath" "$path" + zip -q -b /tmp/ "$zipPathTemp" "$zipPath" + + if [[ $OS == "Mac" ]];then + hash=$(base64 -i "$zipPathTemp") + else + hash=$(base64 -w 0 "$zipPathTemp") + fi + + # remove temporary temp file + rm "$zipPathTemp" + else + echo "Processing file..." + + # Create zip file temporarily to send file + zip -q -b /tmp/ "$zipPath" "$path" + + if [[ $OS == "Mac" ]];then + hash=$(base64 -i "$zipPath") + else + hash=$(base64 -w 0 "$zipPath") + fi + fi + + # remove temporary temp file + rm "$zipPath" + + if [[ $(echo -n "$hash" | wc -m) -gt 32600 ]];then + params="base64zip=paste" + if [[ $OS == "Windows" || $OS == "WSL" || $OS == "WSL2" ]];then + echo -n "$hash" | clip.exe + elif [[ $OS == "Mac" ]];then + echo -n "$hash" | pbcopy + else + (echo -n "$hash" | xclip) || echo "You need to install xclip for sending bigger files from cli" + fi + hash= + fi + + openMoreLoad + exit +} + +############################################################ +############################################################ +# Main program # +############################################################ +############################################################ +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +pushd . > '/dev/null'; +SCRIPTPATH="${BASH_SOURCE[0]:-$0}"; + +while [ -h "$SCRIPTPATH" ]; +do + cd "$( dirname -- "$SCRIPTPATH"; )"; + SCRIPTPATH="$( readlink -f -- "$SCRIPTPATH"; )"; +done + +cd "$( dirname -- "$SCRIPTPATH"; )" > '/dev/null'; +SCRIPTPATH="$( pwd; )"; +popd > '/dev/null'; + +CONFIGPATH="${SCRIPTPATH}/.moreload-cli-config" + +[ ! -f "$CONFIGPATH" ] && + specifyDomain "https://moreload.com/" && + [ ! -f "$CONFIGPATH" ] && + echo "Could not create config file. Add 'DOMAIN=https://moreload.com/' to a file called .moreload-cli-config in the same file as this 'moreload' bash file" + +[ ! -f "$CONFIGPATH" ] || export "$(grep -v '^#' "$CONFIGPATH" | xargs)" + +setOs +############################################################ +# Process the input options. Add options as needed. # +############################################################ +# Get the options +# open MoreLoad if no options are given +[[ $# -eq 0 ]] && openMoreLoad && exit + +# display help and exit if first argument is "--help" or more than 2 arguments are given +[ "$1" == "--help" ] || [[ $# -gt 2 ]] && help && exit + +while getopts "d:ht:*" option; do + case $option in + d) # specify domain + specifyDomain "$2" + exit;; + t) # Send text + sendText + exit;; + h | ?) # display help and exit + help + exit;; + esac +done + +# Send file(s) +# display help and exit if 2 arguments are given or if file does not exist +[[ $# -eq 2 ]] || [[ ! -a $1 ]] && help && exit + +sendFiles "$1" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a22d55b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1104 @@ +{ + "name": "moreload", + "version": "1.7.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "moreload", + "version": "1.7.3", + "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "express-rate-limit": "^6.7.0", + "ua-parser-js": "^1.0.35", + "unique-names-generator": "^4.3.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=15" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-rate-limit": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "engines": { + "node": ">= 12.9.0" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", + "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/unique-names-generator": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", + "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==", + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "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 + } + } + } + }, + "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-rate-limit": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "requires": {} + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "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==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "ua-parser-js": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", + "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==" + }, + "unique-names-generator": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", + "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9295f1 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "moreload", + "version": "1.7.3", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start:prod": "node index.js --rate-limit --auto-restart" + }, + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "express-rate-limit": "^6.7.0", + "ua-parser-js": "^1.0.35", + "unique-names-generator": "^4.3.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=15" + } +} diff --git a/public/images/android-chrome-192x192-maskable.png b/public/images/android-chrome-192x192-maskable.png new file mode 100644 index 0000000..f0e9245 Binary files /dev/null and b/public/images/android-chrome-192x192-maskable.png differ diff --git a/public/images/android-chrome-192x192.png b/public/images/android-chrome-192x192.png new file mode 100644 index 0000000..0bdca51 Binary files /dev/null and b/public/images/android-chrome-192x192.png differ diff --git a/public/images/android-chrome-512x512-maskable.png b/public/images/android-chrome-512x512-maskable.png new file mode 100644 index 0000000..cdda606 Binary files /dev/null and b/public/images/android-chrome-512x512-maskable.png differ diff --git a/public/images/android-chrome-512x512.png b/public/images/android-chrome-512x512.png new file mode 100644 index 0000000..b01e679 Binary files /dev/null and b/public/images/android-chrome-512x512.png differ diff --git a/public/images/apple-touch-icon.png b/public/images/apple-touch-icon.png new file mode 100644 index 0000000..0a32878 Binary files /dev/null and b/public/images/apple-touch-icon.png differ diff --git a/public/images/favicon-96x96-notification.png b/public/images/favicon-96x96-notification.png new file mode 100644 index 0000000..d3407c3 Binary files /dev/null and b/public/images/favicon-96x96-notification.png differ diff --git a/public/images/favicon-96x96.png b/public/images/favicon-96x96.png new file mode 100644 index 0000000..b8a5746 Binary files /dev/null and b/public/images/favicon-96x96.png differ diff --git a/public/images/logo_blue_512x512.png b/public/images/logo_blue_512x512.png new file mode 100644 index 0000000..41d13fc Binary files /dev/null and b/public/images/logo_blue_512x512.png differ diff --git a/public/images/logo_transparent_128x128.png b/public/images/logo_transparent_128x128.png new file mode 100644 index 0000000..c276efe Binary files /dev/null and b/public/images/logo_transparent_128x128.png differ diff --git a/public/images/logo_transparent_512x512.png b/public/images/logo_transparent_512x512.png new file mode 100644 index 0000000..367e24f Binary files /dev/null and b/public/images/logo_transparent_512x512.png differ diff --git a/public/images/logo_transparent_white_512x512.png b/public/images/logo_transparent_white_512x512.png new file mode 100644 index 0000000..37589b6 Binary files /dev/null and b/public/images/logo_transparent_white_512x512.png differ diff --git a/public/images/logo_white_512x512.png b/public/images/logo_white_512x512.png new file mode 100644 index 0000000..d7750d9 Binary files /dev/null and b/public/images/logo_white_512x512.png differ diff --git a/public/images/moreload_screenshot_mobile_1.png b/public/images/moreload_screenshot_mobile_1.png new file mode 100644 index 0000000..d93aafb Binary files /dev/null and b/public/images/moreload_screenshot_mobile_1.png differ diff --git a/public/images/moreload_screenshot_mobile_2.png b/public/images/moreload_screenshot_mobile_2.png new file mode 100644 index 0000000..51ace10 Binary files /dev/null and b/public/images/moreload_screenshot_mobile_2.png differ diff --git a/public/images/moreload_screenshot_mobile_3.png b/public/images/moreload_screenshot_mobile_3.png new file mode 100644 index 0000000..57ad15a Binary files /dev/null and b/public/images/moreload_screenshot_mobile_3.png differ diff --git a/public/images/moreload_screenshot_mobile_4.png b/public/images/moreload_screenshot_mobile_4.png new file mode 100644 index 0000000..d5811ad Binary files /dev/null and b/public/images/moreload_screenshot_mobile_4.png differ diff --git a/public/images/moreload_screenshot_mobile_5.png b/public/images/moreload_screenshot_mobile_5.png new file mode 100644 index 0000000..d205fd9 Binary files /dev/null and b/public/images/moreload_screenshot_mobile_5.png differ diff --git a/public/images/moreload_screenshot_mobile_6.png b/public/images/moreload_screenshot_mobile_6.png new file mode 100644 index 0000000..23c06ae Binary files /dev/null and b/public/images/moreload_screenshot_mobile_6.png differ diff --git a/public/images/moreload_screenshot_mobile_7.png b/public/images/moreload_screenshot_mobile_7.png new file mode 100644 index 0000000..c32980a Binary files /dev/null and b/public/images/moreload_screenshot_mobile_7.png differ diff --git a/public/images/mstile-150x150.png b/public/images/mstile-150x150.png new file mode 100644 index 0000000..6380e32 Binary files /dev/null and b/public/images/mstile-150x150.png differ diff --git a/public/images/safari-pinned-tab.svg b/public/images/safari-pinned-tab.svg new file mode 100644 index 0000000..263ee4e --- /dev/null +++ b/public/images/safari-pinned-tab.svg @@ -0,0 +1,251 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/snapdrop-graphics.sketch b/public/images/snapdrop-graphics.sketch new file mode 100644 index 0000000..b8b756a Binary files /dev/null and b/public/images/snapdrop-graphics.sketch differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f9ece4c --- /dev/null +++ b/public/index.html @@ -0,0 +1,389 @@ + + + + + + + + MoreLoad + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+
+ + + + + +
+ +
+ +
+ + +

Open MoreLoad on other devices to send files

+
Pair devices to be discoverable on other networks
+
+ +

+
+
+ +
+ +
+ You are known as: +
+ + + +
+
+ You can be discovered by everyone on this network + +
+
+ + +
+ + +

Pair Devices

+
+

000 000

+
Input this key on another device
or scan the QR-Code.
+
+
+ + + + + + +
+
Enter key from another device to continue.
+
+ + +
+
+
+
+
+ + +
+ + +

Edit Paired Devices

+
+
+

Activate auto-accept to automatically accept all files sent from that device.

+
+
+ +
+
+
+
+
+ + + + +

+
+
+ + would like to share +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+ + + + +

+
+
+ + has sent +
+
+ + +
+
+
+
+
+
+ + + +
+
+
+
+ + +
+ + +

Send Message

+
+ Send a Message to + +
+
+
+
+ + +
+
+
+
+
+ + + + +

Message Received

+
+ + has sent: +
+
+
+
+ + +
+
+
+
+ + + + + + + + + + + +
+ +
+ + +
+ + + + + +
+
+ +
+

MoreLoad

+
v1.7.3
+
+
The easiest way to transfer files across devices
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..b99b067 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,281 @@ +{ + "name": "MoreLoad", + "short_name": "MoreLoad", + "icons": [{ + "src": "images/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + },{ + "src": "images/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + },{ + "src": "images/android-chrome-192x192-maskable.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + },{ + "src": "images/android-chrome-512x512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + },{ + "src": "images/favicon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }], + "background_color": "#efefef", + "start_url": "/", + "scope": "/", + "display": "minimal-ui", + "theme_color": "#3367d6", + "screenshots" : [ + { + "src": "images/moreload_screenshot_mobile_1.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_2.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_3.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_4.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_5.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_6.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_7.png", + "sizes": "1170x2532", + "type": "image/png" + } + ], + "share_target": { + "action": "/", + "method":"POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [{ + "name": "allfiles", + "accept": ["*/*"] + }] + } + }, + "file_handlers": [ + { + "action": "/?file_handler", + "name": "All Files", + "accept": { + "application/cpl+xml": [".cpl"], + "application/gpx+xml": [".gpx"], + "application/gzip": [".gz"], + "application/java-archive": [".jar", ".war", ".ear"], + "application/java-vm": [".class"], + "application/javascript": [".js", ".mjs"], + "application/json": [".json", ".map"], + "application/manifest+json": [".webmanifest"], + "application/msword": [".doc", ".dot", ".wiz"], + "application/octet-stream": [".bin", ".dms", ".lrf", ".mar", ".so", ".dist", ".distz", ".pkg", ".bpk", ".dump", ".elc", ".deploy", ".exe", ".dll", ".deb", ".dmg", ".iso", ".img", ".msi", ".msp", ".msm", ".buffer"], + "application/oda": [".oda"], + "application/oxps": [".oxps"], + "application/pdf": [".pdf"], + "application/pgp-signature": [".asc", ".sig"], + "application/pics-rules": [".prf"], + "application/pkcs7-mime": [".p7c"], + "application/pkix-cert": [".cer"], + "application/postscript": [".ai", ".eps", ".ps"], + "application/rtf": [".rtf"], + "application/vnd.android.package-archive": [".apk"], + "application/vnd.apple.mpegurl": [".m3u", ".m3u8"], + "application/vnd.apple.pkpass": [".pkpass"], + "application/vnd.google-earth.kml+xml": [".kml"], + "application/vnd.google-earth.kmz": [".kmz"], + "application/vnd.ms-cab-compressed": [".cab"], + "application/vnd.ms-excel": [".xls", ".xlm", ".xla", ".xlc", ".xlt", ".xlw"], + "application/vnd.ms-outlook": [".msg"], + "application/vnd.ms-powerpoint": [".ppt", ".pot", ".ppa", ".pps", ".pwz"], + "application/vnd.ms-project": [".mpp", ".mpt"], + "application/vnd.ms-xpsdocument": [".xps"], + "application/vnd.oasis.opendocument.database": [".odb"], + "application/vnd.oasis.opendocument.spreadsheet": [".ods"], + "application/vnd.oasis.opendocument.text": [".odt"], + "application/vnd.openstreetmap.data+xml": [".osm"], + "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"], + "application/vnd.tcpdump.pcap": [".pcap", ".cap", ".dmp"], + "application/vnd.wordperfect": [".wpd"], + "application/wasm": [".wasm"], + "application/x-7z-compressed": [".7z"], + "application/x-apple-diskimage": [".dmg"], + "application/x-bcpio": [".bcpio"], + "application/x-bittorrent": [".torrent"], + "application/x-cbr": [".cbr", ".cba", ".cbt", ".cbz", ".cb7"], + "application/x-cdlink": [".vcd"], + "application/x-chrome-extension": [".crx"], + "application/x-cpio": [".cpio"], + "application/x-csh": [".csh"], + "application/x-debian-package": [".deb", ".udeb"], + "application/x-dvi": [".dvi"], + "application/x-freearc": [".arc"], + "application/x-gtar": [".gtar"], + "application/x-hdf": [".hdf"], + "application/x-hdf5": [".h5"], + "application/x-httpd-php": [".php"], + "application/x-iso9660-image": [".iso"], + "application/x-iwork-keynote-sffkey": [".key"], + "application/x-iwork-numbers-sffnumbers": [".numbers"], + "application/x-iwork-pages-sffpages": [".pages"], + "application/x-latex": [".latex"], + "application/x-makeself": [".run"], + "application/x-mif": [".mif"], + "application/x-ms-shortcut": [".lnk"], + "application/x-msaccess": [".mdb"], + "application/x-msdownload": [".exe", ".dll", ".com", ".bat", ".msi"], + "application/x-mspublisher": [".pub"], + "application/x-netcdf": [".cdf", ".nc"], + "application/x-perl": [".pl", ".pm"], + "application/x-pilot": [".prc", ".pdb"], + "application/x-pkcs12": [".p12", ".pfx"], + "application/x-pn-realaudio": [".ram"], + "application/x-python-code": [".pyc", ".pyo"], + "application/x-rar-compressed": [".rar"], + "application/x-redhat-package-manager": [".rpm"], + "application/x-sh": [".sh"], + "application/x-shar": [".shar"], + "application/x-shockwave-flash": [".swf"], + "application/x-sql": [".sql"], + "application/x-subrip": [".srt"], + "application/x-sv4cpio": [".sv4cpio"], + "application/x-sv4crc": [".sv4crc"], + "application/x-tads": [".gam"], + "application/x-tar": [".tar"], + "application/x-tcl": [".tcl"], + "application/x-tex": [".tex"], + "application/x-troff": [".roff", ".t", ".tr"], + "application/x-troff-man": [".man"], + "application/x-troff-me": [".me"], + "application/x-troff-ms": [".ms"], + "application/x-ustar": [".ustar"], + "application/x-wais-source": [".src"], + "application/x-xpinstall": [".xpi"], + "application/xhtml+xml": [".xhtml", ".xht"], + "application/xml": [".xsl", ".rdf", ".wsdl", ".xpdl"], + "application/zip": [".zip"], + "audio/3gpp": [".3gp", ".3gpp"], + "audio/3gpp2": [".3g2", ".3gpp2"], + "audio/aac": [".aac", ".adts", ".loas", ".ass"], + "audio/basic": [".au", ".snd"], + "audio/midi": [".mid", ".midi", ".kar", ".rmi"], + "audio/mpeg": [".mpga", ".mp2", ".mp2a", ".mp3", ".m2a", ".m3a"], + "audio/ogg": [".oga", ".ogg", ".spx", ".opus"], + "audio/opus": [".opus"], + "audio/x-aiff": [".aif", ".aifc", ".aiff"], + "audio/x-flac": [".flac"], + "audio/x-m4a": [".m4a"], + "audio/x-mpegurl": [".m3u"], + "audio/x-ms-wma": [".wma"], + "audio/x-pn-realaudio": [".ra"], + "audio/x-wav": [".wav"], + "font/otf": [".otf"], + "font/ttf": [".ttf"], + "font/woff": [".woff"], + "font/woff2": [".woff2"], + "image/emf": [".emf"], + "image/gif": [".gif"], + "image/heic": [".heic"], + "image/heif": [".heif"], + "image/ief": [".ief"], + "image/jpeg": [".jpeg", ".jpg"], + "image/jpg": [".jpg"], + "image/pict": [".pict", ".pct", ".pic"], + "image/png": [".png"], + "image/svg+xml": [".svg", ".svgz"], + "image/tiff": [".tif", ".tiff"], + "image/vnd.adobe.photoshop": [".psd"], + "image/vnd.djvu": [".djvu", ".djv"], + "image/vnd.dwg": [".dwg"], + "image/vnd.dxf": [".dxf"], + "image/vnd.microsoft.icon": [".ico"], + "image/vnd.ms-dds": [".dds"], + "image/x-3ds": [".3ds"], + "image/x-cmu-raster": [".ras"], + "image/x-icon": [".ico"], + "image/x-ms-bmp": [".bmp"], + "image/x-portable-anymap": [".pnm"], + "image/x-portable-bitmap": [".pbm"], + "image/x-portable-graymap": [".pgm"], + "image/x-portable-pixmap": [".ppm"], + "image/x-rgb": [".rgb"], + "image/x-tga": [".tga"], + "image/x-xbitmap": [".xbm"], + "image/x-xpixmap": [".xpm"], + "image/x-xwindowdump": [".xwd"], + "message/rfc822": [".eml", ".mht", ".mhtml", ".nws"], + "model/obj": [".obj"], + "model/stl": [".stl"], + "model/vnd.collada+xml": [".dae"], + "text/calendar": [".ics", ".ifb"], + "text/css": [".css"], + "text/csv": [".csv"], + "text/html": [".html", ".htm", ".shtml"], + "text/markdown": [".markdown", ".md"], + "text/plain": [".txt", ".text", ".conf", ".def", ".list", ".log", ".in", ".ini"], + "text/richtext": [".rtx"], + "text/rtf": [".rtf"], + "text/tab-separated-values": [".tsv"], + "text/x-c": [".c", ".cc", ".cxx", ".cpp", ".h", ".hh", ".dic"], + "text/x-java-source": [".java"], + "text/x-lua": [".lua"], + "text/x-python": [".py"], + "text/x-setext": [".etx"], + "text/x-sgml": [".sgm", ".sgml"], + "text/x-vcard": [".vcf"], + "text/xml": [".xml"], + "text/xul": [".xul"], + "text/yaml": [".yaml", ".yml"], + "video/3gpp": [".3gp", ".3gpp"], + "video/mp2t": [".ts"], + "video/mp4": [".mp4", ".mp4v", ".mpg4"], + "video/mpeg": [".mpeg", ".m1v", ".mpa", ".mpe", ".mpg"], + "video/quicktime": [".mov", ".qt"], + "video/webm": [".webm"], + "video/x-flv": [".flv"], + "video/x-m4v": [".m4v"], + "video/x-ms-asf": [".asf", ".asx"], + "video/x-ms-vob": [".vob"], + "video/x-ms-wmv": [".wmv"], + "video/x-msvideo": [".avi"], + "video/x-sgi-movie": [".*"] + }, + "icons": [ + { + "src": "/images/android-chrome-192x192.png", + "sizes": "192x192" + } + ] + } + ], + "launch_handler": { + "client_mode": "focus-existing" + } +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/scripts/NoSleep.min.js b/public/scripts/NoSleep.min.js new file mode 100644 index 0000000..202c35c --- /dev/null +++ b/public/scripts/NoSleep.min.js @@ -0,0 +1,2 @@ +/*! NoSleep.min.js v0.12.0 - git.io/vfn01 - Rich Tibbett - MIT license */ +!function(A,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.NoSleep=e():A.NoSleep=e()}(this,(function(){return function(A){var e={};function B(g){if(e[g])return e[g].exports;var o=e[g]={i:g,l:!1,exports:{}};return A[g].call(o.exports,o,o.exports,B),o.l=!0,o.exports}return B.m=A,B.c=e,B.d=function(A,e,g){B.o(A,e)||Object.defineProperty(A,e,{enumerable:!0,get:g})},B.r=function(A){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(A,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(A,"__esModule",{value:!0})},B.t=function(A,e){if(1&e&&(A=B(A)),8&e)return A;if(4&e&&"object"==typeof A&&A&&A.__esModule)return A;var g=Object.create(null);if(B.r(g),Object.defineProperty(g,"default",{enumerable:!0,value:A}),2&e&&"string"!=typeof A)for(var o in A)B.d(g,o,function(e){return A[e]}.bind(null,o));return g},B.n=function(A){var e=A&&A.__esModule?function(){return A.default}:function(){return A};return B.d(e,"a",e),e},B.o=function(A,e){return Object.prototype.hasOwnProperty.call(A,e)},B.p="",B(B.s=0)}([function(A,e,B){"use strict";var g=function(){function A(A,e){for(var B=0;B.5&&(e.noSleepVideo.currentTime=Math.random())}))})))}return g(A,[{key:"_addSourceToVideo",value:function(A,e,B){var g=document.createElement("source");g.src=B,g.type="video/"+e,A.appendChild(g)}},{key:"enable",value:function(){var A=this;return Q()?navigator.wakeLock.request("screen").then((function(e){A._wakeLock=e,A.enabled=!0,console.log("Wake Lock active."),A._wakeLock.addEventListener("release",(function(){console.log("Wake Lock released.")}))})).catch((function(e){throw A.enabled=!1,console.error(e.name+", "+e.message),e})):C()?(this.disable(),console.warn("\n NoSleep enabled for older iOS devices. This can interrupt\n active or long-running network requests from completing successfully.\n See https://github.com/richtr/NoSleep.js/issues/15 for more details.\n "),this.noSleepTimer=window.setInterval((function(){document.hidden||(window.location.href=window.location.href.split("#")[0],window.setTimeout(window.stop,0))}),15e3),this.enabled=!0,Promise.resolve()):this.noSleepVideo.play().then((function(e){return A.enabled=!0,e})).catch((function(e){throw A.enabled=!1,e}))}},{key:"disable",value:function(){Q()?(this._wakeLock&&this._wakeLock.release(),this._wakeLock=null):C()?this.noSleepTimer&&(console.warn("\n NoSleep now disabled for older iOS devices.\n "),window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):this.noSleepVideo.pause(),this.enabled=!1}},{key:"isEnabled",get:function(){return this.enabled}}]),A}();A.exports=i},function(A,e,B){"use strict";A.exports={webm:"data:video/webm;base64,GkXfowEAAAAAAAAfQoaBAUL3gQFC8oEEQvOBCEKChHdlYm1Ch4EEQoWBAhhTgGcBAAAAAAAVkhFNm3RALE27i1OrhBVJqWZTrIHfTbuMU6uEFlSua1OsggEwTbuMU6uEHFO7a1OsghV17AEAAAAAAACkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmAQAAAAAAAEUq17GDD0JATYCNTGF2ZjU1LjMzLjEwMFdBjUxhdmY1NS4zMy4xMDBzpJBlrrXf3DCDVB8KcgbMpcr+RImIQJBgAAAAAAAWVK5rAQAAAAAAD++uAQAAAAAAADLXgQFzxYEBnIEAIrWcg3VuZIaFVl9WUDiDgQEj44OEAmJaAOABAAAAAAAABrCBsLqBkK4BAAAAAAAPq9eBAnPFgQKcgQAitZyDdW5khohBX1ZPUkJJU4OBAuEBAAAAAAAAEZ+BArWIQOdwAAAAAABiZIEgY6JPbwIeVgF2b3JiaXMAAAAAAoC7AAAAAAAAgLUBAAAAAAC4AQN2b3JiaXMtAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAxMDExMDEgKFNjaGF1ZmVudWdnZXQpAQAAABUAAABlbmNvZGVyPUxhdmM1NS41Mi4xMDIBBXZvcmJpcyVCQ1YBAEAAACRzGCpGpXMWhBAaQlAZ4xxCzmvsGUJMEYIcMkxbyyVzkCGkoEKIWyiB0JBVAABAAACHQXgUhIpBCCGEJT1YkoMnPQghhIg5eBSEaUEIIYQQQgghhBBCCCGERTlokoMnQQgdhOMwOAyD5Tj4HIRFOVgQgydB6CCED0K4moOsOQghhCQ1SFCDBjnoHITCLCiKgsQwuBaEBDUojILkMMjUgwtCiJqDSTX4GoRnQXgWhGlBCCGEJEFIkIMGQcgYhEZBWJKDBjm4FITLQagahCo5CB+EIDRkFQCQAACgoiiKoigKEBqyCgDIAAAQQFEUx3EcyZEcybEcCwgNWQUAAAEACAAAoEiKpEiO5EiSJFmSJVmSJVmS5omqLMuyLMuyLMsyEBqyCgBIAABQUQxFcRQHCA1ZBQBkAAAIoDiKpViKpWiK54iOCISGrAIAgAAABAAAEDRDUzxHlETPVFXXtm3btm3btm3btm3btm1blmUZCA1ZBQBAAAAQ0mlmqQaIMAMZBkJDVgEACAAAgBGKMMSA0JBVAABAAACAGEoOogmtOd+c46BZDppKsTkdnEi1eZKbirk555xzzsnmnDHOOeecopxZDJoJrTnnnMSgWQqaCa0555wnsXnQmiqtOeeccc7pYJwRxjnnnCateZCajbU555wFrWmOmkuxOeecSLl5UptLtTnnnHPOOeecc84555zqxekcnBPOOeecqL25lpvQxTnnnE/G6d6cEM4555xzzjnnnHPOOeecIDRkFQAABABAEIaNYdwpCNLnaCBGEWIaMulB9+gwCRqDnELq0ehopJQ6CCWVcVJKJwgNWQUAAAIAQAghhRRSSCGFFFJIIYUUYoghhhhyyimnoIJKKqmooowyyyyzzDLLLLPMOuyssw47DDHEEEMrrcRSU2011lhr7jnnmoO0VlprrbVSSimllFIKQkNWAQAgAAAEQgYZZJBRSCGFFGKIKaeccgoqqIDQkFUAACAAgAAAAABP8hzRER3RER3RER3RER3R8RzPESVREiVREi3TMjXTU0VVdWXXlnVZt31b2IVd933d933d+HVhWJZlWZZlWZZlWZZlWZZlWZYgNGQVAAACAAAghBBCSCGFFFJIKcYYc8w56CSUEAgNWQUAAAIACAAAAHAUR3EcyZEcSbIkS9IkzdIsT/M0TxM9URRF0zRV0RVdUTdtUTZl0zVdUzZdVVZtV5ZtW7Z125dl2/d93/d93/d93/d93/d9XQdCQ1YBABIAADqSIymSIimS4ziOJElAaMgqAEAGAEAAAIriKI7jOJIkSZIlaZJneZaomZrpmZ4qqkBoyCoAABAAQAAAAAAAAIqmeIqpeIqoeI7oiJJomZaoqZoryqbsuq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq4LhIasAgAkAAB0JEdyJEdSJEVSJEdygNCQVQCADACAAAAcwzEkRXIsy9I0T/M0TxM90RM901NFV3SB0JBVAAAgAIAAAAAAAAAMybAUy9EcTRIl1VItVVMt1VJF1VNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVN0zRNEwgNWQkAkAEAkBBTLS3GmgmLJGLSaqugYwxS7KWxSCpntbfKMYUYtV4ah5RREHupJGOKQcwtpNApJq3WVEKFFKSYYyoVUg5SIDRkhQAQmgHgcBxAsixAsiwAAAAAAAAAkDQN0DwPsDQPAAAAAAAAACRNAyxPAzTPAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAA0DwP8DwR8EQRAAAAAAAAACzPAzTRAzxRBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAAsDwP8EQR0DwRAAAAAAAAACzPAzxRBDzRAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEOAAABBgIRQasiIAiBMAcEgSJAmSBM0DSJYFTYOmwTQBkmVB06BpME0AAAAAAAAAAAAAJE2DpkHTIIoASdOgadA0iCIAAAAAAAAAAAAAkqZB06BpEEWApGnQNGgaRBEAAAAAAAAAAAAAzzQhihBFmCbAM02IIkQRpgkAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAGHAAAAgwoQwUGrIiAIgTAHA4imUBAIDjOJYFAACO41gWAABYliWKAABgWZooAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAYcAAACDChDBQashIAiAIAcCiKZQHHsSzgOJYFJMmyAJYF0DyApgFEEQAIAAAocAAACLBBU2JxgEJDVgIAUQAABsWxLE0TRZKkaZoniiRJ0zxPFGma53meacLzPM80IYqiaJoQRVE0TZimaaoqME1VFQAAUOAAABBgg6bE4gCFhqwEAEICAByKYlma5nmeJ4qmqZokSdM8TxRF0TRNU1VJkqZ5niiKommapqqyLE3zPFEURdNUVVWFpnmeKIqiaaqq6sLzPE8URdE0VdV14XmeJ4qiaJqq6roQRVE0TdNUTVV1XSCKpmmaqqqqrgtETxRNU1Vd13WB54miaaqqq7ouEE3TVFVVdV1ZBpimaaqq68oyQFVV1XVdV5YBqqqqruu6sgxQVdd1XVmWZQCu67qyLMsCAAAOHAAAAoygk4wqi7DRhAsPQKEhKwKAKAAAwBimFFPKMCYhpBAaxiSEFEImJaXSUqogpFJSKRWEVEoqJaOUUmopVRBSKamUCkIqJZVSAADYgQMA2IGFUGjISgAgDwCAMEYpxhhzTiKkFGPOOScRUoox55yTSjHmnHPOSSkZc8w556SUzjnnnHNSSuacc845KaVzzjnnnJRSSuecc05KKSWEzkEnpZTSOeecEwAAVOAAABBgo8jmBCNBhYasBABSAQAMjmNZmuZ5omialiRpmud5niiapiZJmuZ5nieKqsnzPE8URdE0VZXneZ4oiqJpqirXFUXTNE1VVV2yLIqmaZqq6rowTdNUVdd1XZimaaqq67oubFtVVdV1ZRm2raqq6rqyDFzXdWXZloEsu67s2rIAAPAEBwCgAhtWRzgpGgssNGQlAJABAEAYg5BCCCFlEEIKIYSUUggJAAAYcAAACDChDBQashIASAUAAIyx1lprrbXWQGettdZaa62AzFprrbXWWmuttdZaa6211lJrrbXWWmuttdZaa6211lprrbXWWmuttdZaa6211lprrbXWWmuttdZaa6211lprrbXWWmstpZRSSimllFJKKaWUUkoppZRSSgUA+lU4APg/2LA6wknRWGChISsBgHAAAMAYpRhzDEIppVQIMeacdFRai7FCiDHnJKTUWmzFc85BKCGV1mIsnnMOQikpxVZjUSmEUlJKLbZYi0qho5JSSq3VWIwxqaTWWoutxmKMSSm01FqLMRYjbE2ptdhqq7EYY2sqLbQYY4zFCF9kbC2m2moNxggjWywt1VprMMYY3VuLpbaaizE++NpSLDHWXAAAd4MDAESCjTOsJJ0VjgYXGrISAAgJACAQUooxxhhzzjnnpFKMOeaccw5CCKFUijHGnHMOQgghlIwx5pxzEEIIIYRSSsaccxBCCCGEkFLqnHMQQgghhBBKKZ1zDkIIIYQQQimlgxBCCCGEEEoopaQUQgghhBBCCKmklEIIIYRSQighlZRSCCGEEEIpJaSUUgohhFJCCKGElFJKKYUQQgillJJSSimlEkoJJYQSUikppRRKCCGUUkpKKaVUSgmhhBJKKSWllFJKIYQQSikFAAAcOAAABBhBJxlVFmGjCRcegEJDVgIAZAAAkKKUUiktRYIipRikGEtGFXNQWoqocgxSzalSziDmJJaIMYSUk1Qy5hRCDELqHHVMKQYtlRhCxhik2HJLoXMOAAAAQQCAgJAAAAMEBTMAwOAA4XMQdAIERxsAgCBEZohEw0JweFAJEBFTAUBigkIuAFRYXKRdXECXAS7o4q4DIQQhCEEsDqCABByccMMTb3jCDU7QKSp1IAAAAAAADADwAACQXAAREdHMYWRobHB0eHyAhIiMkAgAAAAAABcAfAAAJCVAREQ0cxgZGhscHR4fICEiIyQBAIAAAgAAAAAggAAEBAQAAAAAAAIAAAAEBB9DtnUBAAAAAAAEPueBAKOFggAAgACjzoEAA4BwBwCdASqwAJAAAEcIhYWIhYSIAgIABhwJ7kPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99YAD+/6tQgKOFggADgAqjhYIAD4AOo4WCACSADqOZgQArADECAAEQEAAYABhYL/QACIBDmAYAAKOFggA6gA6jhYIAT4AOo5mBAFMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAGSADqOFggB6gA6jmYEAewAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIAj4AOo5mBAKMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAKSADqOFggC6gA6jmYEAywAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIAz4AOo4WCAOSADqOZgQDzADECAAEQEAAYABhYL/QACIBDmAYAAKOFggD6gA6jhYIBD4AOo5iBARsAEQIAARAQFGAAYWC/0AAiAQ5gGACjhYIBJIAOo4WCATqADqOZgQFDADECAAEQEAAYABhYL/QACIBDmAYAAKOFggFPgA6jhYIBZIAOo5mBAWsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAXqADqOFggGPgA6jmYEBkwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIBpIAOo4WCAbqADqOZgQG7ADECAAEQEAAYABhYL/QACIBDmAYAAKOFggHPgA6jmYEB4wAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIB5IAOo4WCAfqADqOZgQILADECAAEQEAAYABhYL/QACIBDmAYAAKOFggIPgA6jhYICJIAOo5mBAjMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAjqADqOFggJPgA6jmYECWwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYICZIAOo4WCAnqADqOZgQKDADECAAEQEAAYABhYL/QACIBDmAYAAKOFggKPgA6jhYICpIAOo5mBAqsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCArqADqOFggLPgA6jmIEC0wARAgABEBAUYABhYL/QACIBDmAYAKOFggLkgA6jhYIC+oAOo5mBAvsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAw+ADqOZgQMjADECAAEQEAAYABhYL/QACIBDmAYAAKOFggMkgA6jhYIDOoAOo5mBA0sAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCA0+ADqOFggNkgA6jmYEDcwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIDeoAOo4WCA4+ADqOZgQObADECAAEQEAAYABhYL/QACIBDmAYAAKOFggOkgA6jhYIDuoAOo5mBA8MAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCA8+ADqOFggPkgA6jhYID+oAOo4WCBA+ADhxTu2sBAAAAAAAAEbuPs4EDt4r3gQHxghEr8IEK",mp4:"data:video/mp4;base64,AAAAHGZ0eXBNNFYgAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAGF21kYXTeBAAAbGliZmFhYyAxLjI4AABCAJMgBDIARwAAArEGBf//rdxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNDIgcjIgOTU2YzhkOCAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMTQgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDE6MHgxMTEgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCB2YnZfbWF4cmF0ZT03NjggdmJ2X2J1ZnNpemU9MzAwMCBjcmZfbWF4PTAuMCBuYWxfaHJkPW5vbmUgZmlsbGVyPTAgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAFZliIQL8mKAAKvMnJycnJycnJycnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXiEASZACGQAjgCEASZACGQAjgAAAAAdBmjgX4GSAIQBJkAIZACOAAAAAB0GaVAX4GSAhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGagC/AySEASZACGQAjgAAAAAZBmqAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZrAL8DJIQBJkAIZACOAAAAABkGa4C/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmwAvwMkhAEmQAhkAI4AAAAAGQZsgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGbQC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm2AvwMkhAEmQAhkAI4AAAAAGQZuAL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGboC/AySEASZACGQAjgAAAAAZBm8AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZvgL8DJIQBJkAIZACOAAAAABkGaAC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmiAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpAL8DJIQBJkAIZACOAAAAABkGaYC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmoAvwMkhAEmQAhkAI4AAAAAGQZqgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGawC/AySEASZACGQAjgAAAAAZBmuAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZsAL8DJIQBJkAIZACOAAAAABkGbIC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm0AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZtgL8DJIQBJkAIZACOAAAAABkGbgCvAySEASZACGQAjgCEASZACGQAjgAAAAAZBm6AnwMkhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AAAAhubW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAABDcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzB0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAA+kAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAALAAAACQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAPpAAAAAAABAAAAAAKobWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAB1MAAAdU5VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACU21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAhNzdGJsAAAAr3N0c2QAAAAAAAAAAQAAAJ9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAALAAkABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAALWF2Y0MBQsAN/+EAFWdCwA3ZAsTsBEAAAPpAADqYA8UKkgEABWjLg8sgAAAAHHV1aWRraEDyXyRPxbo5pRvPAyPzAAAAAAAAABhzdHRzAAAAAAAAAAEAAAAeAAAD6QAAABRzdHNzAAAAAAAAAAEAAAABAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAAIxzdHN6AAAAAAAAAAAAAAAeAAADDwAAAAsAAAALAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAiHN0Y28AAAAAAAAAHgAAAEYAAANnAAADewAAA5gAAAO0AAADxwAAA+MAAAP2AAAEEgAABCUAAARBAAAEXQAABHAAAASMAAAEnwAABLsAAATOAAAE6gAABQYAAAUZAAAFNQAABUgAAAVkAAAFdwAABZMAAAWmAAAFwgAABd4AAAXxAAAGDQAABGh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAABDcAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAQkAAADcAABAAAAAAPgbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAC7gAAAykBVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAADi21pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAADT3N0YmwAAABnc3RzZAAAAAAAAAABAAAAV21wNGEAAAAAAAAAAQAAAAAAAAAAAAIAEAAAAAC7gAAAAAAAM2VzZHMAAAAAA4CAgCIAAgAEgICAFEAVBbjYAAu4AAAADcoFgICAAhGQBoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAADIAAAQAAAAAAQAAAkAAAAFUc3RzYwAAAAAAAAAbAAAAAQAAAAEAAAABAAAAAgAAAAIAAAABAAAAAwAAAAEAAAABAAAABAAAAAIAAAABAAAABgAAAAEAAAABAAAABwAAAAIAAAABAAAACAAAAAEAAAABAAAACQAAAAIAAAABAAAACgAAAAEAAAABAAAACwAAAAIAAAABAAAADQAAAAEAAAABAAAADgAAAAIAAAABAAAADwAAAAEAAAABAAAAEAAAAAIAAAABAAAAEQAAAAEAAAABAAAAEgAAAAIAAAABAAAAFAAAAAEAAAABAAAAFQAAAAIAAAABAAAAFgAAAAEAAAABAAAAFwAAAAIAAAABAAAAGAAAAAEAAAABAAAAGQAAAAIAAAABAAAAGgAAAAEAAAABAAAAGwAAAAIAAAABAAAAHQAAAAEAAAABAAAAHgAAAAIAAAABAAAAHwAAAAQAAAABAAAA4HN0c3oAAAAAAAAAAAAAADMAAAAaAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAACMc3RjbwAAAAAAAAAfAAAALAAAA1UAAANyAAADhgAAA6IAAAO+AAAD0QAAA+0AAAQAAAAEHAAABC8AAARLAAAEZwAABHoAAASWAAAEqQAABMUAAATYAAAE9AAABRAAAAUjAAAFPwAABVIAAAVuAAAFgQAABZ0AAAWwAAAFzAAABegAAAX7AAAGFwAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTUuMzMuMTAw"}}])})); diff --git a/public/scripts/QRCode.min.js b/public/scripts/QRCode.min.js new file mode 100644 index 0000000..569a867 --- /dev/null +++ b/public/scripts/QRCode.min.js @@ -0,0 +1,2 @@ +/*! qrcode-svg v1.1.0 | https://github.com/papnkukn/qrcode-svg | MIT license */ +function QR8bitByte(t){this.mode=QRMode.MODE_8BIT_BYTE,this.data=t,this.parsedData=[];for(var e=0,r=this.data.length;e65536?(o[0]=240|(1835008&n)>>>18,o[1]=128|(258048&n)>>>12,o[2]=128|(4032&n)>>>6,o[3]=128|63&n):n>2048?(o[0]=224|(61440&n)>>>12,o[1]=128|(4032&n)>>>6,o[2]=128|63&n):n>128?(o[0]=192|(1984&n)>>>6,o[1]=128|63&n):o[0]=n,this.parsedData.push(o)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function QRCodeModel(t,e){this.typeNumber=t,this.errorCorrectLevel=e,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}QR8bitByte.prototype={getLength:function(t){return this.parsedData.length},write:function(t){for(var e=0,r=this.parsedData.length;e=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,e)},setupPositionProbePattern:function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(this.modules[t+r][e+o]=0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4)},getBestMaskPattern:function(){for(var t=0,e=0,r=0;r<8;r++){this.makeImpl(!0,r);var o=QRUtil.getLostPoint(this);(0==r||t>o)&&(t=o,e=r)}return e},createMovieClip:function(t,e,r){var o=t.createEmptyMovieClip(e,r);this.make();for(var n=0;n>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++){o=!t&&1==(e>>r&1);this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o}},setupTypeInfo:function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=QRUtil.getBCHTypeInfo(r),n=0;n<15;n++){var i=!t&&1==(o>>n&1);n<6?this.modules[n][8]=i:n<8?this.modules[n+1][8]=i:this.modules[this.moduleCount-15+n][8]=i}for(n=0;n<15;n++){i=!t&&1==(o>>n&1);n<8?this.modules[8][this.moduleCount-n-1]=i:n<9?this.modules[8][15-n-1+1]=i:this.modules[8][15-n-1]=i}this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,n=7,i=0,a=this.moduleCount-1;a>0;a-=2)for(6==a&&a--;;){for(var s=0;s<2;s++)if(null==this.modules[o][a-s]){var h=!1;i>>n&1)),QRUtil.getMask(e,o,a-s)&&(h=!h),this.modules[o][a-s]=h,-1==--n&&(i++,n=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},QRCodeModel.PAD0=236,QRCodeModel.PAD1=17,QRCodeModel.createData=function(t,e,r){for(var o=QRRSBlock.getRSBlocks(t,e),n=new QRBitBuffer,i=0;i8*s)throw new Error("code length overflow. ("+n.getLengthInBits()+">"+8*s+")");for(n.getLengthInBits()+4<=8*s&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(!1);for(;!(n.getLengthInBits()>=8*s||(n.put(QRCodeModel.PAD0,8),n.getLengthInBits()>=8*s));)n.put(QRCodeModel.PAD1,8);return QRCodeModel.createBytes(n,o)},QRCodeModel.createBytes=function(t,e){for(var r=0,o=0,n=0,i=new Array(e.length),a=new Array(e.length),s=0;s=0?d.get(f):0}}var c=0;for(u=0;u=0;)e^=QRUtil.G15<=0;)e^=QRUtil.G18<>>=1;return e},getPatternPosition:function(t){return QRUtil.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case QRMaskPattern.PATTERN000:return(e+r)%2==0;case QRMaskPattern.PATTERN001:return e%2==0;case QRMaskPattern.PATTERN010:return r%3==0;case QRMaskPattern.PATTERN011:return(e+r)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(e/2)+Math.floor(r/3))%2==0;case QRMaskPattern.PATTERN101:return e*r%2+e*r%3==0;case QRMaskPattern.PATTERN110:return(e*r%2+e*r%3)%2==0;case QRMaskPattern.PATTERN111:return(e*r%3+(e+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new QRPolynomial([1],0),r=0;r5&&(r+=3+i-5)}for(o=0;o=256;)t-=255;return QRMath.EXP_TABLE[t]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},i=0;i<8;i++)QRMath.EXP_TABLE[i]=1<>>7-t%8&1)},put:function(t,e){for(var r=0;r>>e-r-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];function QRCode(t){if(this.options={padding:4,width:256,height:256,typeNumber:4,color:"#000000",background:"#ffffff",ecl:"M"},"string"==typeof t&&(t={content:t}),t)for(var e in t)this.options[e]=t[e];if("string"!=typeof this.options.content)throw new Error("Expected 'content' as string!");if(0===this.options.content.length)throw new Error("Expected 'content' to be non-empty!");if(!(this.options.padding>=0))throw new Error("Expected 'padding' value to be non-negative!");if(!(this.options.width>0&&this.options.height>0))throw new Error("Expected 'width' or 'height' value to be higher than zero!");var r=this.options.content,o=function(t,e){for(var r=function(t){var e=encodeURI(t).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return e.length+(e.length!=t?3:0)}(t),o=1,n=0,i=0,a=QRCodeLimitLength.length;i<=a;i++){var s=QRCodeLimitLength[i];if(!s)throw new Error("Content too long: expected "+n+" but got "+r);switch(e){case"L":n=s[0];break;case"M":n=s[1];break;case"Q":n=s[2];break;case"H":n=s[3];break;default:throw new Error("Unknwon error correction level: "+e)}if(r<=n)break;o++}if(o>QRCodeLimitLength.length)throw new Error("Content too long");return o}(r,this.options.ecl),n=function(t){switch(t){case"L":return QRErrorCorrectLevel.L;case"M":return QRErrorCorrectLevel.M;case"Q":return QRErrorCorrectLevel.Q;case"H":return QRErrorCorrectLevel.H;default:throw new Error("Unknwon error correction level: "+t)}}(this.options.ecl);this.qrcode=new QRCodeModel(o,n),this.qrcode.addData(r),this.qrcode.make()}QRCode.prototype.svg=function(t){var e=this.options||{},r=this.qrcode.modules;void 0===t&&(t={container:e.container||"svg"});for(var o=void 0===e.pretty||!!e.pretty,n=o?" ":"",i=o?"\r\n":"",a=e.width,s=e.height,h=r.length,l=a/(h+2*e.padding),u=s/(h+2*e.padding),g=void 0!==e.join&&!!e.join,d=void 0!==e.swap&&!!e.swap,f=void 0===e.xmlDeclaration||!!e.xmlDeclaration,c=void 0!==e.predefined&&!!e.predefined,R=c?n+''+i:"",p=n+''+i,m="",Q="",v=0;v'+i:n+''+i}}g&&(m=n+'');var T="";switch(t.container){case"svg":f&&(T+=''+i),T+=''+i,T+=R+p+m,T+="";break;case"svg-viewbox":f&&(T+=''+i),T+=''+i,T+=R+p+m,T+="";break;case"g":T+=''+i,T+=R+p+m,T+="";break;default:T+=(R+p+m).replace(/^\s+/,"")}return T},QRCode.prototype.save=function(t,e){var r=this.svg();"function"!=typeof e&&(e=function(t,e){});try{require("fs").writeFile(t,r,e)}catch(t){e(t)}},"undefined"!=typeof module&&(module.exports=QRCode); \ No newline at end of file diff --git a/public/scripts/network.js b/public/scripts/network.js new file mode 100644 index 0000000..1cf044b --- /dev/null +++ b/public/scripts/network.js @@ -0,0 +1,1073 @@ +window.URL = window.URL || window.webkitURL; +window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); + +if (!window.isRtcSupported) alert("WebRTC must be enabled for MoreLoad to work"); + +window.hiddenProperty = 'hidden' in document ? 'hidden' : + 'webkitHidden' in document ? 'webkitHidden' : + 'mozHidden' in document ? 'mozHidden' : + null; +window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' : + 'webkitvisibilitychange' in document ? 'webkitvisibilitychange' : + 'mozvisibilitychange' in document ? 'mozvisibilitychange' : + null; + +class ServerConnection { + + constructor() { + this._connect(); + Events.on('pagehide', _ => this._disconnect()); + document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange()); + if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); + Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail })); + Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'})); + Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail})); + Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail})); + Events.on('resend-peers', _ => this.send({ type: 'resend-peers'})); + Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate()); + Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail)); + Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' })); + Events.on('offline', _ => clearTimeout(this._reconnectTimer)); + Events.on('online', _ => this._connect()); + } + + _connect() { + clearTimeout(this._reconnectTimer); + if (this._isConnected() || this._isConnecting()) return; + const ws = new WebSocket(this._endpoint()); + ws.binaryType = 'arraybuffer'; + ws.onopen = _ => this._onOpen(); + ws.onmessage = e => this._onMessage(e.data); + ws.onclose = _ => this._onDisconnect(); + ws.onerror = e => this._onError(e); + this._socket = ws; + } + + _onOpen() { + console.log('WS: server connected'); + Events.fire('ws-connected'); + if (this._isReconnect) Events.fire('notify-user', 'Connected.'); + } + + _onPairDeviceInitiate() { + if (!this._isConnected()) { + Events.fire('notify-user', 'You need to be online to pair devices.'); + return; + } + this.send({ type: 'pair-device-initiate' }) + } + + _onPairDeviceJoin(roomKey) { + if (!this._isConnected()) { + setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000); + return; + } + this.send({ type: 'pair-device-join', roomKey: roomKey }) + } + + _setRtcConfig(config) { + window.rtcConfig = config; + } + + _onMessage(msg) { + msg = JSON.parse(msg); + if (msg.type !== 'ping') console.log('WS receive:', msg); + switch (msg.type) { + case 'rtc-config': + this._setRtcConfig(msg.config); + break; + case 'peers': + this._onPeers(msg); + break; + case 'peer-joined': + Events.fire('peer-joined', msg); + break; + case 'peer-left': + Events.fire('peer-left', msg); + break; + case 'signal': + Events.fire('signal', msg); + break; + case 'ping': + this.send({ type: 'pong' }); + break; + case 'display-name': + this._onDisplayName(msg); + break; + case 'pair-device-initiated': + Events.fire('pair-device-initiated', msg); + break; + case 'pair-device-joined': + Events.fire('pair-device-joined', msg); + break; + case 'pair-device-join-key-invalid': + Events.fire('pair-device-join-key-invalid'); + break; + case 'pair-device-canceled': + Events.fire('pair-device-canceled', msg.roomKey); + break; + case 'pair-device-join-key-rate-limit': + Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.'); + break; + case 'secret-room-deleted': + Events.fire('secret-room-deleted', msg.roomSecret); + break; + case 'room-secret-regenerated': + Events.fire('room-secret-regenerated', msg); + break; + default: + console.error('WS receive: unknown message type', msg); + } + } + + send(msg) { + if (!this._isConnected()) return; + if (msg.type !== 'pong') console.log("WS send:", msg) + this._socket.send(JSON.stringify(msg)); + } + + _onPeers(msg) { + Events.fire('peers', msg); + } + + _onDisplayName(msg) { + // Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload + sessionStorage.setItem("peerId", msg.message.peerId); + sessionStorage.setItem("peerIdHash", msg.message.peerIdHash); + + // Add peerId to localStorage to mark it for other MoreLoad tabs on the same browser + BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => { + if (!peerId) return; + console.log("successfully added peerId to localStorage"); + + // Only now join rooms + Events.fire('join-ip-room'); + PersistentStorage.getAllRoomSecrets().then(roomSecrets => { + Events.fire('room-secrets', roomSecrets); + }); + }); + + Events.fire('display-name', msg); + } + + _endpoint() { + // hack to detect if deployment or development environment + const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws'; + const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback'; + let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc); + const peerId = sessionStorage.getItem("peerId"); + const peerIdHash = sessionStorage.getItem("peerIdHash"); + if (peerId && peerIdHash) { + ws_url.searchParams.append('peer_id', peerId); + ws_url.searchParams.append('peer_id_hash', peerIdHash); + } + return ws_url.toString(); + } + + _disconnect() { + this.send({ type: 'disconnect' }); + + const peerId = sessionStorage.getItem("peerId"); + BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => { + console.log("successfully removed peerId from localStorage"); + }); + + if (!this._socket) return; + + this._socket.onclose = null; + this._socket.close(); + this._socket = null; + Events.fire('ws-disconnected'); + this._isReconnect = true; + } + + _onDisconnect() { + console.log('WS: server disconnected'); + Events.fire('notify-user', 'Connecting..'); + clearTimeout(this._reconnectTimer); + this._reconnectTimer = setTimeout(_ => this._connect(), 1000); + Events.fire('ws-disconnected'); + this._isReconnect = true; + } + + _onVisibilityChange() { + if (window.hiddenProperty) return; + this._connect(); + } + + _isConnected() { + return this._socket && this._socket.readyState === this._socket.OPEN; + } + + _isConnecting() { + return this._socket && this._socket.readyState === this._socket.CONNECTING; + } + + _onError(e) { + console.error(e); + } + + _reconnect() { + this._disconnect(); + this._connect(); + } +} + +class Peer { + + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { + this._server = serverConnection; + this._isCaller = isCaller; + this._peerId = peerId; + this._roomType = roomType; + this._updateRoomSecret(roomSecret); + + this._filesQueue = []; + this._busy = false; + + // evaluate auto accept + this._evaluateAutoAccept(); + } + + sendJSON(message) { + this._send(JSON.stringify(message)); + } + + sendDisplayName(displayName) { + this.sendJSON({type: 'display-name-changed', displayName: displayName}); + } + + _isSameBrowser() { + return BrowserTabsConnector.peerIsSameBrowser(this._peerId); + } + + _updateRoomSecret(roomSecret) { + // if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets + // -> do not delete duplicates and do not regenerate room secrets + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) { + // remove old roomSecrets to prevent multiple pairings with same peer + PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => { + if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret); + }) + } + + this._roomSecret = roomSecret; + + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) { + // increase security by increasing roomSecret length + console.log('RoomSecret is regenerated to increase security') + Events.fire('regenerate-room-secret', this._roomSecret); + } + } + + _evaluateAutoAccept() { + if (!this._roomSecret) { + this._setAutoAccept(false); + return; + } + + PersistentStorage.getRoomSecretEntry(this._roomSecret) + .then(roomSecretEntry => { + const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false; + this._setAutoAccept(autoAccept); + }) + .catch(_ => { + this._setAutoAccept(false); + }); + } + + _setAutoAccept(autoAccept) { + this._autoAccept = autoAccept; + } + + getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) { + return new Promise((resolve, reject) => { + let image = new Image(); + image.src = URL.createObjectURL(file); + image.onload = _ => { + let imageWidth = image.width; + let imageHeight = image.height; + let canvas = document.createElement('canvas'); + + // resize the canvas and draw the image data into it + if (width && height) { + canvas.width = width; + canvas.height = height; + } else if (width) { + canvas.width = width; + canvas.height = Math.floor(imageHeight * width / imageWidth) + } else if (height) { + canvas.width = Math.floor(imageWidth * height / imageHeight); + canvas.height = height; + } else { + canvas.width = imageWidth; + canvas.height = imageHeight + } + + var ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0, canvas.width, canvas.height); + + let dataUrl = canvas.toDataURL("image/jpeg", quality); + resolve(dataUrl); + } + image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`); + }).then(dataUrl => { + return dataUrl; + }).catch(e => console.error(e)); + } + + async requestFileTransfer(files) { + let header = []; + let totalSize = 0; + let imagesOnly = true + for (let i=0; i this._send(chunk), + offset => this._onPartitionEnd(offset)); + this._chunker.nextPartition(); + } + + _onPartitionEnd(offset) { + this.sendJSON({ type: 'partition', offset: offset }); + } + + _onReceivedPartitionEnd(offset) { + this.sendJSON({ type: 'partition-received', offset: offset }); + } + + _sendNextPartition() { + if (!this._chunker || this._chunker.isFileEnd()) return; + this._chunker.nextPartition(); + } + + _sendProgress(progress) { + this.sendJSON({ type: 'progress', progress: progress }); + } + + _onMessage(message) { + if (typeof message !== 'string') { + this._onChunkReceived(message); + return; + } + const messageJSON = JSON.parse(message); + switch (messageJSON.type) { + case 'request': + this._onFilesTransferRequest(messageJSON); + break; + case 'header': + this._onFileHeader(messageJSON); + break; + case 'partition': + this._onReceivedPartitionEnd(messageJSON); + break; + case 'partition-received': + this._sendNextPartition(); + break; + case 'progress': + this._onDownloadProgress(messageJSON.progress); + break; + case 'files-transfer-response': + this._onFileTransferRequestResponded(messageJSON); + break; + case 'file-transfer-complete': + this._onFileTransferCompleted(); + break; + case 'message-transfer-complete': + this._onMessageTransferCompleted(); + break; + case 'text': + this._onTextReceived(messageJSON); + break; + case 'display-name-changed': + this._onDisplayNameChanged(messageJSON); + break; + } + } + + _onFilesTransferRequest(request) { + if (this._requestPending) { + // Only accept one request at a time per peer + this.sendJSON({type: 'files-transfer-response', accepted: false}); + return; + } + if (window.iOS && request.totalSize >= 200*1024*1024) { + // iOS Safari can only put 400MB at once to memory. + // Request to send them in chunks of 200MB instead: + this.sendJSON({type: 'files-transfer-response', accepted: false, reason: 'ios-memory-limit'}); + return; + } + + this._requestPending = request; + + if (this._autoAccept) { + // auto accept if set via Edit Paired Devices Dialog + this._respondToFileTransferRequest(true); + return; + } + + // default behavior: show user transfer request + Events.fire('files-transfer-request', { + request: request, + peerId: this._peerId + }); + } + + _respondToFileTransferRequest(accepted) { + this.sendJSON({type: 'files-transfer-response', accepted: accepted}); + if (accepted) { + this._requestAccepted = this._requestPending; + this._totalBytesReceived = 0; + this._busy = true; + this._filesReceived = []; + } + this._requestPending = null; + } + + _onFileHeader(header) { + if (this._requestAccepted && this._requestAccepted.header.length) { + this._lastProgress = 0; + this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, + this._requestAccepted.totalSize, + this._totalBytesReceived, + fileBlob => this._onFileReceived(fileBlob) + ); + } + } + + _abortTransfer() { + Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); + Events.fire('notify-user', 'Files are incorrect.'); + this._filesReceived = []; + this._requestAccepted = null; + this._digester = null; + throw new Error("Received files differ from requested files. Abort!"); + } + + _onChunkReceived(chunk) { + if(!this._digester || !(chunk.byteLength || chunk.size)) return; + + this._digester.unchunk(chunk); + const progress = this._digester.progress; + + if (progress > 1) { + this._abortTransfer(); + } + + this._onDownloadProgress(progress); + + // occasionally notify sender about our progress + if (progress - this._lastProgress < 0.005 && progress !== 1) return; + this._lastProgress = progress; + this._sendProgress(progress); + } + + _onDownloadProgress(progress) { + Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'}); + } + + async _onFileReceived(fileBlob) { + const acceptedHeader = this._requestAccepted.header.shift(); + this._totalBytesReceived += fileBlob.size; + + this.sendJSON({type: 'file-transfer-complete'}); + + const sameSize = fileBlob.size === acceptedHeader.size; + const sameName = fileBlob.name === acceptedHeader.name + if (!sameSize || !sameName) { + this._abortTransfer(); + } + + // include for compatibility with Snapdrop for Android app + Events.fire('file-received', fileBlob); + + this._filesReceived.push(fileBlob); + if (!this._requestAccepted.header.length) { + this._busy = false; + Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); + Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize}); + this._filesReceived = []; + this._requestAccepted = null; + } + } + + _onFileTransferCompleted() { + this._chunker = null; + if (!this._filesQueue.length) { + this._busy = false; + Events.fire('notify-user', 'File transfer completed.'); + } else { + this._dequeueFile(); + } + } + + _onFileTransferRequestResponded(message) { + if (!message.accepted) { + Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); + this._filesRequested = null; + if (message.reason === 'ios-memory-limit') { + Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); + } + return; + } + Events.fire('file-transfer-accepted'); + Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'transfer'}); + this.sendFiles(); + } + + _onMessageTransferCompleted() { + Events.fire('notify-user', 'Message transfer completed.'); + } + + sendText(text) { + const unescaped = btoa(unescape(encodeURIComponent(text))); + this.sendJSON({ type: 'text', text: unescaped }); + } + + _onTextReceived(message) { + if (!message.text) return; + const escaped = decodeURIComponent(escape(atob(message.text))); + Events.fire('text-received', { text: escaped, peerId: this._peerId }); + this.sendJSON({ type: 'message-transfer-complete' }); + } + + _onDisplayNameChanged(message) { + const displayNameHasChanged = this._displayName !== message.displayName + + if (message.displayName && displayNameHasChanged) { + this._displayName = message.displayName; + } + + Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); + + if (!displayNameHasChanged) return; + Events.fire('notify-peer-display-name-changed', this._peerId); + } +} + +class RTCPeer extends Peer { + + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { + super(serverConnection, isCaller, peerId, roomType, roomSecret); + this.rtcSupported = true; + if (!this._isCaller) return; // we will listen for a caller + this._connect(); + } + + _connect() { + if (!this._conn || this._conn.signalingState === "closed") this._openConnection(); + + if (this._isCaller) { + this._openChannel(); + } else { + this._conn.ondatachannel = e => this._onChannelOpened(e); + } + } + + _openConnection() { + this._conn = new RTCPeerConnection(window.rtcConfig); + this._conn.onicecandidate = e => this._onIceCandidate(e); + this._conn.onicecandidateerror = e => this._onError(e); + this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); + this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); + } + + _openChannel() { + if (!this._conn) return; + const channel = this._conn.createDataChannel('data-channel', { + ordered: true, + reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable + }); + channel.onopen = e => this._onChannelOpened(e); + channel.onerror = e => this._onError(e); + this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e)); + } + + _onDescription(description) { + // description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400'); + this._conn.setLocalDescription(description) + .then(_ => this._sendSignal({ sdp: description })) + .catch(e => this._onError(e)); + } + + _onIceCandidate(event) { + if (!event.candidate) return; + this._sendSignal({ ice: event.candidate }); + } + + onServerMessage(message) { + if (!this._conn) this._connect(); + + if (message.sdp) { + this._conn.setRemoteDescription(message.sdp) + .then( _ => { + if (message.sdp.type === 'offer') { + return this._conn.createAnswer() + .then(d => this._onDescription(d)); + } + }) + .catch(e => this._onError(e)); + } else if (message.ice) { + this._conn.addIceCandidate(new RTCIceCandidate(message.ice)) + .catch(e => this._onError(e)); + } + } + + _onChannelOpened(event) { + console.log('RTC: channel opened with', this._peerId); + const channel = event.channel || event.target; + channel.binaryType = 'arraybuffer'; + channel.onmessage = e => this._onMessage(e.data); + channel.onclose = _ => this._onChannelClosed(); + this._channel = channel; + Events.on('beforeunload', e => this._onBeforeUnload(e)); + Events.on('pagehide', _ => this._onPageHide()); + Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()}); + } + + _onMessage(message) { + if (typeof message === 'string') { + console.log('RTC:', JSON.parse(message)); + } + super._onMessage(message); + } + + getConnectionHash() { + const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); + const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); + let localConnectionFingerprint, remoteConnectionFingerprint; + for (let i=0; i this._onMessage(e.detail)); + Events.on('peers', e => this._onPeers(e.detail)); + Events.on('files-selected', e => this._onFilesSelected(e.detail)); + Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) + Events.on('send-text', e => this._onSendText(e.detail)); + Events.on('peer-left', e => this._onPeerLeft(e.detail)); + Events.on('peer-joined', e => this._onPeerJoined(e.detail)); + Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId)); + Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); + Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail)); + Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); + Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); + Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail)); + Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept)); + } + + _onMessage(message) { + const peerId = message.sender.id; + this.peers[peerId].onServerMessage(message); + } + + _refreshPeer(peer, roomType, roomSecret) { + if (!peer) return false; + + const roomTypeIsSecret = roomType === "secret"; + const roomSecretsDiffer = peer._roomSecret !== roomSecret; + + // if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept + if (roomTypeIsSecret && roomSecretsDiffer) { + peer._updateRoomSecret(roomSecret); + peer._evaluateAutoAccept(); + + return true; + } + + const roomTypesDiffer = peer._roomType !== roomType; + + // if roomTypes differ peer is already connected -> abort + if (roomTypesDiffer) return true; + + peer.refresh(); + + return true; + } + + _createOrRefreshPeer(isCaller, peerId, roomType, roomSecret) { + const peer = this.peers[peerId]; + if (peer) { + this._refreshPeer(peer, roomType, roomSecret); + return; + } + + this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret); + } + + _onPeerJoined(message) { + this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret); + } + + _onPeers(message) { + message.peers.forEach(peer => { + this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret); + }) + } + + _onRespondToFileTransferRequest(detail) { + this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); + } + + _onFilesSelected(message) { + let inputFiles = Array.from(message.files); + delete message.files; + let files = []; + const l = inputFiles.length; + for (let i=0; i { + if (!peerIds) return; + console.log("successfully removed other peerIds from localStorage"); + }); + } + } + } + + _onPeerConnected(peerId) { + this._notifyPeerDisplayNameChanged(peerId); + } + + _onPeerDisconnected(peerId) { + const peer = this.peers[peerId]; + delete this.peers[peerId]; + if (!peer || !peer._conn) return; + if (peer._channel) peer._channel.onclose = null; + peer._conn.close(); + peer._busy = false; + } + + _onSecretRoomDeleted(roomSecret) { + for (const peerId in this.peers) { + const peer = this.peers[peerId]; + if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) { + this._onPeerDisconnected(peerId); + } + } + } + + _onRoomSecretRegenerated(message) { + PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => { + console.log("successfully regenerated room secret"); + Events.fire("room-secrets", [message.newRoomSecret]); + }) + } + + _notifyPeersDisplayNameChanged(newDisplayName) { + this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName; + for (const peerId in this.peers) { + this._notifyPeerDisplayNameChanged(peerId); + } + } + + _notifyPeerDisplayNameChanged(peerId) { + const peer = this.peers[peerId]; + if (!peer) return; + this.peers[peerId].sendDisplayName(this._displayName); + } + + _onDisplayName(displayName) { + this._originalDisplayName = displayName; + // if the displayName has not been changed (yet) set the displayName to the original displayName + if (!this._displayName) this._displayName = displayName; + } + + _onAutoAcceptUpdated(roomSecret, autoAccept) { + const peerId = this._getPeerIdFromRoomSecret(roomSecret); + if (!peerId) return; + this.peers[peerId]._setAutoAccept(autoAccept); + } + + _getPeerIdFromRoomSecret(roomSecret) { + for (const peerId in this.peers) { + const peer = this.peers[peerId]; + // peer must have same roomSecret and not be on the same browser. + if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) { + return peer._peerId; + } + } + return false; + } +} + +class FileChunker { + + constructor(file, onChunk, onPartitionEnd) { + this._chunkSize = 64000; // 64 KB + this._maxPartitionSize = 1e6; // 1 MB + this._offset = 0; + this._partitionSize = 0; + this._file = file; + this._onChunk = onChunk; + this._onPartitionEnd = onPartitionEnd; + this._reader = new FileReader(); + this._reader.addEventListener('load', e => this._onChunkRead(e.target.result)); + } + + nextPartition() { + this._partitionSize = 0; + this._readChunk(); + } + + _readChunk() { + const chunk = this._file.slice(this._offset, this._offset + this._chunkSize); + this._reader.readAsArrayBuffer(chunk); + } + + _onChunkRead(chunk) { + this._offset += chunk.byteLength; + this._partitionSize += chunk.byteLength; + this._onChunk(chunk); + if (this.isFileEnd()) return; + if (this._isPartitionEnd()) { + this._onPartitionEnd(this._offset); + return; + } + this._readChunk(); + } + + repeatPartition() { + this._offset -= this._partitionSize; + this.nextPartition(); + } + + _isPartitionEnd() { + return this._partitionSize >= this._maxPartitionSize; + } + + isFileEnd() { + return this._offset >= this._file.size; + } +} + +class FileDigester { + + constructor(meta, totalSize, totalBytesReceived, callback) { + this._buffer = []; + this._bytesReceived = 0; + this._size = meta.size; + this._name = meta.name; + this._mime = meta.mime; + this._totalSize = totalSize; + this._totalBytesReceived = totalBytesReceived; + this._callback = callback; + } + + unchunk(chunk) { + this._buffer.push(chunk); + this._bytesReceived += chunk.byteLength || chunk.size; + this.progress = (this._totalBytesReceived + this._bytesReceived) / this._totalSize; + if (isNaN(this.progress)) this.progress = 1 + + if (this._bytesReceived < this._size) return; + // we are done + const blob = new Blob(this._buffer) + this._buffer = null; + this._callback(new File([blob], this._name, { + type: this._mime, + lastModified: new Date().getTime() + })); + } + +} + +class Events { + static fire(type, detail) { + window.dispatchEvent(new CustomEvent(type, { detail: detail })); + } + + static on(type, callback, options = false) { + return window.addEventListener(type, callback, options); + } + + static off(type, callback, options = false) { + return window.removeEventListener(type, callback, options); + } +} diff --git a/public/scripts/theme.js b/public/scripts/theme.js new file mode 100644 index 0000000..81cf0b2 --- /dev/null +++ b/public/scripts/theme.js @@ -0,0 +1,78 @@ +(function(){ + + const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches; + + const $themeAuto = document.getElementById('theme-auto'); + const $themeLight = document.getElementById('theme-light'); + const $themeDark = document.getElementById('theme-dark'); + + let currentTheme = localStorage.getItem('theme'); + + if (currentTheme === 'dark') { + setModeToDark(); + } else if (currentTheme === 'light') { + setModeToLight(); + } + + $themeAuto.addEventListener('click', _ => { + if (currentTheme) { + setModeToAuto(); + } else { + setModeToDark(); + } + }); + $themeLight.addEventListener('click', _ => { + if (currentTheme !== 'light') { + setModeToLight(); + } else { + setModeToAuto(); + } + }); + $themeDark.addEventListener('click', _ => { + if (currentTheme !== 'dark') { + setModeToDark(); + } else { + setModeToLight(); + } + }); + + function setModeToDark() { + document.body.classList.remove('light-theme'); + document.body.classList.add('dark-theme'); + localStorage.setItem('theme', 'dark'); + currentTheme = 'dark'; + + $themeAuto.classList.remove("selected"); + $themeLight.classList.remove("selected"); + $themeDark.classList.add("selected"); + } + + function setModeToLight() { + document.body.classList.remove('dark-theme'); + document.body.classList.add('light-theme'); + localStorage.setItem('theme', 'light'); + currentTheme = 'light'; + + $themeAuto.classList.remove("selected"); + $themeLight.classList.add("selected"); + $themeDark.classList.remove("selected"); + } + + function setModeToAuto() { + document.body.classList.remove('dark-theme'); + document.body.classList.remove('light-theme'); + if (prefersDarkTheme) { + document.body.classList.add('dark-theme'); + } else if (prefersLightTheme) { + document.body.classList.add('light-theme'); + } + localStorage.removeItem('theme'); + currentTheme = undefined; + + $themeAuto.classList.add("selected"); + $themeLight.classList.remove("selected"); + $themeDark.classList.remove("selected"); + } + +})(); diff --git a/public/scripts/ui.js b/public/scripts/ui.js new file mode 100644 index 0000000..2549eeb --- /dev/null +++ b/public/scripts/ui.js @@ -0,0 +1,2314 @@ +const $ = query => document.getElementById(query); +const $$ = query => document.body.querySelector(query); +window.isProductionEnvironment = !window.location.host.startsWith('localhost'); +window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; +window.android = /android/i.test(navigator.userAgent); +window.pasteMode = {}; +window.pasteMode.activated = false; + +// set display name +Events.on('display-name', e => { + const me = e.detail.message; + const $displayName = $('display-name'); + $displayName.setAttribute('placeholder', me.displayName); +}); + +class PeersUI { + + constructor() { + Events.on('peer-joined', e => this._onPeerJoined(e.detail)); + Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash)); + Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); + Events.on('peers', e => this._onPeers(e.detail)); + Events.on('set-progress', e => this._onSetProgress(e.detail)); + Events.on('paste', e => this._onPaste(e)); + Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); + this.peers = {}; + + this.$cancelPasteModeBtn = $('cancel-paste-mode'); + this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); + + Events.on('dragover', e => this._onDragOver(e)); + Events.on('dragleave', _ => this._onDragEnd()); + Events.on('dragend', _ => this._onDragEnd()); + + Events.on('drop', e => this._onDrop(e)); + Events.on('keydown', e => this._onKeyDown(e)); + + this.$xPeers = $$('x-peers'); + this.$xNoPeers = $$('x-no-peers'); + this.$xInstructions = $$('x-instructions'); + + Events.on('peer-added', _ => this.evaluateOverflowing()); + Events.on('bg-resize', _ => this.evaluateOverflowing()); + + this.$displayName = $('display-name'); + + this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e)); + this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e)); + this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText)); + + Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail)); + Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e)); + + // Load saved display name on page load + this._getSavedDisplayName().then(displayName => { + console.log("Retrieved edited display name:", displayName) + if (displayName) Events.fire('self-display-name-changed', displayName); + }); + + + /* prevent animation on load */ + setTimeout(_ => { + this.$xNoPeers.style.animationIterationCount = "1"; + }, 300); + } + + _insertDisplayName(displayName) { + this.$displayName.textContent = displayName; + } + + _onKeyDownDisplayName(e) { + if (e.key === "Enter" || e.key === "Escape") { + e.preventDefault(); + e.target.blur(); + } + } + + _onKeyUpDisplayName(e) { + // fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty + if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = ''; + } + + async _saveDisplayName(newDisplayName) { + newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') + const savedDisplayName = await this._getSavedDisplayName(); + if (newDisplayName === savedDisplayName) return; + + if (newDisplayName) { + PersistentStorage.set('editedDisplayName', newDisplayName) + .then(_ => { + Events.fire('notify-user', 'Device name is changed permanently.'); + }) + .catch(_ => { + console.log("This browser does not support IndexedDB. Use localStorage instead."); + localStorage.setItem('editedDisplayName', newDisplayName); + Events.fire('notify-user', 'Device name is changed only for this session.'); + }) + .finally(_ => { + Events.fire('self-display-name-changed', newDisplayName); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName}); + }); + } else { + PersistentStorage.delete('editedDisplayName') + .catch(_ => { + console.log("This browser does not support IndexedDB. Use localStorage instead.") + localStorage.removeItem('editedDisplayName'); + Events.fire('notify-user', 'Random Display name is used again.'); + }) + .finally(_ => { + Events.fire('notify-user', 'Device name is randomly generated again.'); + Events.fire('self-display-name-changed', ''); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); + }); + } + } + + _getSavedDisplayName() { + return new Promise((resolve) => { + PersistentStorage.get('editedDisplayName') + .then(displayName => { + if (!displayName) displayName = ""; + resolve(displayName); + }) + .catch(_ => { + let displayName = localStorage.getItem('editedDisplayName'); + if (!displayName) displayName = ""; + resolve(displayName); + }) + }); + } + + _changePeerDisplayName(peerId, displayName) { + this.peers[peerId].name.displayName = displayName; + const peerIdNode = $(peerId); + if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; + this._redrawPeerRoomTypes(peerId); + } + + _onPeerDisplayNameChanged(e) { + if (!e.detail.displayName) return; + this._changePeerDisplayName(e.detail.peerId, e.detail.displayName); + } + + _onKeyDown(e) { + if (document.querySelectorAll('x-dialog[show]').length === 0 && window.pasteMode.activated && e.code === "Escape") { + Events.fire('deactivate-paste-mode'); + } + } + + _onPeerJoined(msg) { + this._joinPeer(msg.peer, msg.roomType, msg.roomSecret); + } + + _joinPeer(peer, roomType, roomSecret) { + const existingPeer = this.peers[peer.id]; + if (existingPeer) { + // peer already exists. Abort but add roomType to GUI and update roomSecret + // skip if peer is a tab on the same browser + if (!existingPeer.sameBrowser()) { + // add roomType to PeerUI + if (!existingPeer.roomTypes.includes(roomType)) { + existingPeer.roomTypes.push(roomType); + } + this._redrawPeerRoomTypes(peer.id); + + if (roomType === "secret") existingPeer.roomSecret = roomSecret; + } + return; + } + peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id); + + if (!(roomType === "secret" && peer.sameBrowser())) { + peer.roomTypes = [roomType]; + peer.roomSecret = roomSecret; + } + + this.peers[peer.id] = peer; + } + + _onPeerConnected(peerId, connectionHash) { + if (!this.peers[peerId] || $(peerId)) return; + + const peer = this.peers[peerId]; + + new PeerUI(peer, connectionHash); + } + + _redrawPeerRoomTypes(peerId) { + const peer = this.peers[peerId] + const peerNode = $(peerId); + if (!peerNode) return; + peerNode.classList.remove('type-ip', 'type-secret'); + if (!peer.sameBrowser()) { + peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`)); + } + } + + evaluateOverflowing() { + if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) { + this.$xPeers.classList.add('overflowing'); + } else { + this.$xPeers.classList.remove('overflowing'); + } + } + + _onPeers(msg) { + msg.peers.forEach(peer => this._joinPeer(peer, msg.roomType, msg.roomSecret)); + } + + _onPeerDisconnected(peerId) { + const $peer = $(peerId); + if (!$peer) return; + $peer.remove(); + this.evaluateOverflowing(); + } + + _onSecretRoomDeleted(roomSecret) { + for (const peerId in this.peers) { + const peer = this.peers[peerId]; + if (peer.roomSecret === roomSecret) { + let index = peer.roomTypes.indexOf('secret'); + peer.roomTypes.splice(index, 1); + peer.roomSecret = ""; + + if (peer.roomTypes.length) { + this._redrawPeerRoomTypes(peerId) + return; + } + + this._onPeerDisconnected(peerId); + return; + } + } + } + + _onSetProgress(progress) { + const $peer = $(progress.peerId); + if (!$peer) return; + $peer.ui.setProgress(progress.progress, progress.status) + } + + _onDrop(e) { + e.preventDefault(); + if (!$$('x-peer') || !$$('x-peer').contains(e.target)) { + this._activatePasteMode(e.dataTransfer.files, '') + } + this._onDragEnd(); + } + + _onDragOver(e) { + e.preventDefault(); + this.$xInstructions.setAttribute('drop-bg', 1); + this.$xNoPeers.setAttribute('drop-bg', 1); + } + + _onDragEnd() { + this.$xInstructions.removeAttribute('drop-bg', 1); + this.$xNoPeers.removeAttribute('drop-bg'); + } + + _onPaste(e) { + if(document.querySelectorAll('x-dialog[show]').length === 0) { + // prevent send on paste when dialog is open + e.preventDefault() + const files = e.clipboardData.files; + const text = e.clipboardData.getData("Text"); + if (files.length === 0 && text.length === 0) return; + this._activatePasteMode(files, text); + } + } + + _activatePasteMode(files, text) { + if (!window.pasteMode.activated && (files.length > 0 || text.length > 0)) { + let descriptor; + let noPeersMessage; + + if (files.length === 1) { + descriptor = files[0].name; + noPeersMessage = `Open MoreLoad on other devices to send
${descriptor}`; + } else if (files.length > 1) { + descriptor = `${files[0].name} and ${files.length-1} other files`; + noPeersMessage = `Open MoreLoad on other devices to send
${descriptor}`; + } else { + descriptor = "shared text"; + noPeersMessage = `Open MoreLoad on other devices to send
${descriptor}`; + } + + this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + this.$xInstructions.querySelector('p').style.display = 'block'; + this.$xInstructions.setAttribute('desktop', `Click to send`); + this.$xInstructions.setAttribute('mobile', `Tap to send`); + + this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; + + const _callback = (e) => this._sendClipboardData(e, files, text); + Events.on('paste-pointerdown', _callback); + Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true }); + + this.$cancelPasteModeBtn.removeAttribute('hidden'); + + window.pasteMode.descriptor = descriptor; + window.pasteMode.activated = true; + + console.log('Paste mode activated.'); + Events.fire('paste-mode-changed'); + } + } + + _cancelPasteMode() { + Events.fire('deactivate-paste-mode'); + } + + _deactivatePasteMode(_callback) { + if (window.pasteMode.activated) { + window.pasteMode.descriptor = undefined; + window.pasteMode.activated = false; + Events.off('paste-pointerdown', _callback); + + this.$xInstructions.querySelector('p').innerText = ''; + this.$xInstructions.querySelector('p').style.display = 'none'; + + this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); + this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); + + this.$xNoPeers.querySelector('h2').innerHTML = 'Open MoreLoad on other devices to send files'; + + this.$cancelPasteModeBtn.setAttribute('hidden', ""); + + console.log('Paste mode deactivated.') + Events.fire('paste-mode-changed'); + } + } + + _sendClipboardData(e, files, text) { + // send the pasted file/text content + const peerId = e.detail.peerId; + + if (files.length > 0) { + Events.fire('files-selected', { + files: files, + to: peerId + }); + } else if (text.length > 0) { + Events.fire('send-text', { + text: text, + to: peerId + }); + } + } +} + +class PeerUI { + + constructor(peer, connectionHash) { + this._peer = peer; + this._connectionHash = + `${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`; + this._initDom(); + this._bindListeners(); + + $$('x-peers').appendChild(this.$el) + Events.fire('peer-added'); + this.$xInstructions = $$('x-instructions'); + } + + html() { + let title; + let input = ''; + if (window.pasteMode.activated) { + title = `Click to send ${window.pasteMode.descriptor}`; + } else { + title = 'Click to send files or right click to send a message'; + input = ''; + } + this.$el.innerHTML = ` + `; + + this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); + this.$el.querySelector('.name').textContent = this._displayName(); + this.$el.querySelector('.device-name').textContent = this._deviceName(); + this.$el.querySelector('.connection-hash').textContent = this._connectionHash; + } + + _initDom() { + this.$el = document.createElement('x-peer'); + this.$el.id = this._peer.id; + this.$el.ui = this; + this.$el.classList.add('center'); + if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`)); + this.html(); + + this._callbackInput = e => this._onFilesSelected(e) + this._callbackClickSleep = _ => NoSleepUI.enable() + this._callbackTouchStartSleep = _ => NoSleepUI.enable() + this._callbackDrop = e => this._onDrop(e) + this._callbackDragEnd = e => this._onDragEnd(e) + this._callbackDragLeave = e => this._onDragEnd(e) + this._callbackDragOver = e => this._onDragOver(e) + this._callbackContextMenu = e => this._onRightClick(e) + this._callbackTouchStart = e => this._onTouchStart(e) + this._callbackTouchEnd = e => this._onTouchEnd(e) + this._callbackPointerDown = e => this._onPointerDown(e) + // PasteMode + Events.on('paste-mode-changed', _ => this._onPasteModeChanged()); + } + + _onPasteModeChanged() { + this.html(); + this._bindListeners(); + } + + _bindListeners() { + if(!window.pasteMode.activated) { + // Remove Events Paste Mode + this.$el.removeEventListener('pointerdown', this._callbackPointerDown); + + // Add Events Normal Mode + this.$el.querySelector('input').addEventListener('change', this._callbackInput); + this.$el.addEventListener('click', this._callbackClickSleep); + this.$el.addEventListener('touchstart', this._callbackTouchStartSleep); + this.$el.addEventListener('drop', this._callbackDrop); + this.$el.addEventListener('dragend', this._callbackDragEnd); + this.$el.addEventListener('dragleave', this._callbackDragLeave); + this.$el.addEventListener('dragover', this._callbackDragOver); + this.$el.addEventListener('contextmenu', this._callbackContextMenu); + this.$el.addEventListener('touchstart', this._callbackTouchStart); + this.$el.addEventListener('touchend', this._callbackTouchEnd); + } else { + // Remove Events Normal Mode + this.$el.removeEventListener('click', this._callbackClickSleep); + this.$el.removeEventListener('touchstart', this._callbackTouchStartSleep); + this.$el.removeEventListener('drop', this._callbackDrop); + this.$el.removeEventListener('dragend', this._callbackDragEnd); + this.$el.removeEventListener('dragleave', this._callbackDragLeave); + this.$el.removeEventListener('dragover', this._callbackDragOver); + this.$el.removeEventListener('contextmenu', this._callbackContextMenu); + this.$el.removeEventListener('touchstart', this._callbackTouchStart); + this.$el.removeEventListener('touchend', this._callbackTouchEnd); + + // Add Events Paste Mode + this.$el.addEventListener('pointerdown', this._callbackPointerDown); + } + } + + _onPointerDown(e) { + // Prevents triggering of event twice on touch devices + e.stopPropagation(); + e.preventDefault(); + Events.fire('paste-pointerdown', { + peerId: this._peer.id + }); + } + + _displayName() { + return this._peer.name.displayName; + } + + _deviceName() { + return this._peer.name.deviceName; + } + + _icon() { + const device = this._peer.name.device || this._peer.name; + if (device.type === 'mobile') { + return '#phone-iphone'; + } + if (device.type === 'tablet') { + return '#tablet-mac'; + } + return '#desktop-mac'; + } + + _onFilesSelected(e) { + const $input = e.target; + const files = $input.files; + Events.fire('files-selected', { + files: files, + to: this._peer.id + }); + $input.files = null; // reset input + } + + setProgress(progress, status) { + const $progress = this.$el.querySelector('.progress'); + if (0.5 < progress && progress < 1) { + $progress.classList.add('over50'); + } else { + $progress.classList.remove('over50'); + } + if (progress < 1) { + this.$el.setAttribute('status', status); + } else { + this.$el.removeAttribute('status'); + progress = 0; + } + const degrees = `rotate(${360 * progress}deg)`; + $progress.style.setProperty('--progress', degrees); + } + + _onDrop(e) { + e.preventDefault(); + Events.fire('files-selected', { + files: e.dataTransfer.files, + to: this._peer.id + }); + this._onDragEnd(); + } + + _onDragOver() { + this.$el.setAttribute('drop', 1); + this.$xInstructions.setAttribute('drop-peer', 1); + } + + _onDragEnd() { + this.$el.removeAttribute('drop'); + this.$xInstructions.removeAttribute('drop-peer', 1); + } + + _onRightClick(e) { + e.preventDefault(); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); + } + + _onTouchStart(e) { + this._touchStart = Date.now(); + this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610); + } + + _onTouchEnd(e) { + if (Date.now() - this._touchStart < 500) { + clearTimeout(this._touchTimer); + } else if (this._touchTimer) { // this was a long tap + e.preventDefault(); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); + } + this._touchTimer = null; + } +} + +class Dialog { + constructor(id) { + this.$el = $(id); + this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', _ => this.hide())); + this.$autoFocus = this.$el.querySelector('[autofocus]'); + Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); + } + + show() { + this.$el.setAttribute('show', 1); + if (this.$autoFocus) this.$autoFocus.focus(); + } + + isShown() { + return !!this.$el.attributes["show"]; + } + + hide() { + this.$el.removeAttribute('show'); + if (this.$autoFocus) { + document.activeElement.blur(); + window.blur(); + } + document.title = 'MoreLoad'; + document.changeFavicon("images/favicon-96x96.png"); + this.correspondingPeerId = undefined; + } + + _onPeerDisconnected(peerId) { + if (this.isShown() && this.correspondingPeerId === peerId) { + this.hide(); + Events.fire('notify-user', 'Selected peer left.') + } + } +} + +class ReceiveDialog extends Dialog { + constructor(id) { + super(id); + this.$fileDescription = this.$el.querySelector('.file-description'); + this.$displayName = this.$el.querySelector('.display-name'); + this.$fileStem = this.$el.querySelector('.file-stem'); + this.$fileExtension = this.$el.querySelector('.file-extension'); + this.$fileOther = this.$el.querySelector('.file-other'); + this.$fileSize = this.$el.querySelector('.file-size'); + this.$previewBox = this.$el.querySelector('.file-preview'); + this.$receiveTitle = this.$el.querySelector('h2:first-of-type'); + } + + _formatFileSize(bytes) { + // 1 GB = 1024 MB = 1024^2 KB = 1024^3 B + // 1024^2 = 104876; 1024^3 = 1073741824 + if (bytes >= 1073741824) { + return Math.round(10 * bytes / 1073741824) / 10 + ' GB'; + } else if (bytes >= 1048576) { + return Math.round(bytes / 1048576) + ' MB'; + } else if (bytes > 1024) { + return Math.round(bytes / 1024) + ' KB'; + } else { + return bytes + ' Bytes'; + } + } + + _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { + if (files.length > 1) { + let fileOtherText = ` and ${files.length - 1} other `; + if (files.length === 2) { + fileOtherText += imagesOnly ? 'image' : 'file'; + } else { + fileOtherText += imagesOnly ? 'images' : 'files'; + } + this.$fileOther.innerText = fileOtherText; + } + + const fileName = files[0].name; + const fileNameSplit = fileName.split('.'); + const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; + this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); + this.$fileExtension.innerText = fileExtension; + this.$displayName.innerText = displayName; + this.$displayName.title = connectionHash; + this.$fileSize.innerText = this._formatFileSize(totalSize); + } +} + +class ReceiveFileDialog extends ReceiveDialog { + + constructor() { + super('receive-file-dialog'); + + this.$downloadBtn = this.$el.querySelector('#download-btn'); + this.$shareBtn = this.$el.querySelector('#share-btn'); + + Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize)); + this._filesQueue = []; + } + + _onFilesReceived(sender, files, imagesOnly, totalSize) { + const displayName = $(sender).ui._displayName(); + const connectionHash = $(sender).ui._connectionHash; + this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); + this._nextFiles(); + window.blop.play(); + } + + _nextFiles() { + if (this._busy) return; + this._busy = true; + const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift(); + this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize); + } + + _dequeueFile() { + if (!this._filesQueue.length) { // nothing to do + this._busy = false; + return; + } + // dequeue next file + setTimeout(_ => { + this._busy = false; + this._nextFiles(); + }, 300); + } + + createPreviewElement(file) { + return new Promise((resolve, reject) => { + try { + let mime = file.type.split('/')[0] + let previewElement = { + image: 'img', + audio: 'audio', + video: 'video' + } + + if (Object.keys(previewElement).indexOf(mime) === -1) { + resolve(false); + } else { + let element = document.createElement(previewElement[mime]); + element.controls = true; + element.onload = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onloadeddata = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onerror = _ => { + reject(`${mime} preview could not be loaded from type ${file.type}`); + }; + element.src = URL.createObjectURL(file); + } + } catch (e) { + reject(`preview could not be loaded from type ${file.type}`); + } + }); + } + + async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) { + this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize); + + let descriptor, url, filenameDownload; + if (files.length === 1) { + descriptor = imagesOnly ? 'Image' : 'File'; + } else { + descriptor = imagesOnly ? 'Images' : 'Files'; + } + this.$receiveTitle.innerText = `${descriptor} Received`; + + const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); + if (canShare) { + this.$shareBtn.removeAttribute('hidden'); + this.$shareBtn.onclick = _ => { + navigator.share({files: files}) + .catch(err => { + console.error(err); + }); + } + } + + let downloadZipped = false; + if (files.length > 1) { + downloadZipped = true; + try { + let bytesCompleted = 0; + zipper.createNewZipWriter(); + for (let i=0; i { + Events.fire('set-progress', { + peerId: peerId, + progress: (bytesCompleted + progress) / totalSize, + status: 'process' + }) + } + }); + bytesCompleted += files[i].size; + } + url = await zipper.getBlobURL(); + + let now = new Date(Date.now()); + let year = now.getFullYear().toString(); + let month = (now.getMonth()+1).toString(); + month = month.length < 2 ? "0" + month : month; + let date = now.getDate().toString(); + date = date.length < 2 ? "0" + date : date; + let hours = now.getHours().toString(); + hours = hours.length < 2 ? "0" + hours : hours; + let minutes = now.getMinutes().toString(); + minutes = minutes.length < 2 ? "0" + minutes : minutes; + filenameDownload = `MoreLoad_files_${year+month+date}_${hours+minutes}.zip`; + } catch (e) { + console.error(e); + downloadZipped = false; + } + } + + this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.onclick = _ => { + if (downloadZipped) { + let tmpZipBtn = document.createElement("a"); + tmpZipBtn.download = filenameDownload; + tmpZipBtn.href = url; + tmpZipBtn.click(); + } else { + this._downloadFilesIndividually(files); + } + + if (!canShare) { + this.$downloadBtn.innerText = "Download again"; + } + Events.fire('notify-user', `${descriptor} downloaded successfully`); + this.$downloadBtn.style.pointerEvents = "none"; + setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); + }; + + document.title = files.length === 1 + ? 'File received - MoreLoad' + : `${files.length} Files received - MoreLoad`; + document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) + this.show(); + + setTimeout(_ => { + if (canShare) { + this.$shareBtn.click(); + } else { + this.$downloadBtn.click(); + } + }, 500); + + this.createPreviewElement(files[0]) + .then(canPreview => { + if (canPreview) { + console.log('the file is able to preview'); + } else { + console.log('the file is not able to preview'); + } + }) + .catch(r => console.error(r)); + } + + _downloadFilesIndividually(files) { + let tmpBtn = document.createElement("a"); + for (let i=0; i this._respondToFileTransferRequest(true)); + this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false)); + + Events.on('files-transfer-request', e => this._onRequestFileTransfer(e.detail.request, e.detail.peerId)) + Events.on('keydown', e => this._onKeyDown(e)); + this._filesTransferRequestQueue = []; + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this._respondToFileTransferRequest(false); + } + } + + _onRequestFileTransfer(request, peerId) { + this._filesTransferRequestQueue.push({request: request, peerId: peerId}); + if (this.isShown()) return; + this._dequeueRequests(); + } + + _dequeueRequests() { + if (!this._filesTransferRequestQueue.length) return; + let {request, peerId} = this._filesTransferRequestQueue.shift(); + this._showRequestDialog(request, peerId) + } + + _showRequestDialog(request, peerId) { + this.correspondingPeerId = peerId; + + const displayName = $(peerId).ui._displayName(); + const connectionHash = $(peerId).ui._connectionHash; + this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize); + + if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") { + let element = document.createElement('img'); + element.src = request.thumbnailDataUrl; + this.$previewBox.appendChild(element) + } + + this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` + + document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - MoreLoad`; + document.changeFavicon("images/favicon-96x96-notification.png"); + this.show(); + } + + _respondToFileTransferRequest(accepted) { + Events.fire('respond-to-files-transfer-request', { + to: this.correspondingPeerId, + accepted: accepted + }) + if (accepted) { + Events.fire('set-progress', {peerId: this.correspondingPeerId, progress: 0, status: 'wait'}); + NoSleepUI.enable(); + } + this.hide(); + } + + hide() { + // clear previewBox after dialog is closed + setTimeout(_ => this.$previewBox.innerHTML = '', 300); + + super.hide(); + + // show next request + setTimeout(_ => this._dequeueRequests(), 500); + } +} + +class PairDeviceDialog extends Dialog { + constructor() { + super('pair-device-dialog'); + this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input'); + this.$submitBtn = this.$el.querySelector('button[type="submit"]'); + this.$roomKey = this.$el.querySelector('#room-key'); + this.$qrCode = this.$el.querySelector('#room-key-qr-code'); + this.$pairDeviceBtn = $('pair-device'); + this.$editPairedDevicesBtn = $('edit-paired-devices'); + this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); + this.$createJoinForm = this.$el.querySelector('form'); + + this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e)); + this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate()); + + this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel()) + this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); + this.$inputRoomKeyChars.forEach(el => el.addEventListener('keydown', e => this._onCharsKeyDown(e))); + this.$inputRoomKeyChars.forEach(el => el.addEventListener('focus', e => e.target.select())); + this.$inputRoomKeyChars.forEach(el => el.addEventListener('click', e => e.target.select())); + + Events.on('keydown', e => this._onKeyDown(e)); + Events.on('ws-connected', _ => this._onWsConnected()); + Events.on('ws-disconnected', _ => this.hide()); + Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail)); + Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret)); + Events.on('peers', e => this._onPeers(e.detail)); + Events.on('peer-joined', e => this._onPeerJoined(e.detail)); + Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid()); + Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail)); + Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets()) + Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + this.$el.addEventListener('paste', e => this._onPaste(e)); + + this.evaluateRoomKeyChars(); + this.evaluateUrlAttributes(); + + this.pairPeer = {}; + } + + _onCharsInput(e) { + e.target.value = e.target.value.replace(/\D/g,''); + if (!e.target.value) return; + this.evaluateRoomKeyChars(); + + let nextSibling = e.target.nextElementSibling; + if (nextSibling) { + e.preventDefault(); + nextSibling.focus(); + } + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + // Timeout to prevent paste mode from getting cancelled simultaneously + setTimeout(_ => this._pairDeviceCancel(), 50); + } + } + + _onCharsKeyDown(e) { + let previousSibling = e.target.previousElementSibling; + let nextSibling = e.target.nextElementSibling; + if (e.key === "Backspace" && previousSibling && !e.target.value) { + previousSibling.value = ''; + previousSibling.focus(); + } else if (e.key === "ArrowRight" && nextSibling) { + e.preventDefault(); + nextSibling.focus(); + } else if (e.key === "ArrowLeft" && previousSibling) { + e.preventDefault(); + previousSibling.focus(); + } + } + + _onPaste(e) { + e.preventDefault(); + let num = e.clipboardData.getData("Text").replace(/\D/g,'').substring(0, 6); + for (let i = 0; i < num.length; i++) { + document.activeElement.value = num.charAt(i); + let nextSibling = document.activeElement.nextElementSibling; + if (!nextSibling) break; + nextSibling.focus(); + } + this.evaluateRoomKeyChars(); + } + + evaluateRoomKeyChars() { + if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) { + this.$submitBtn.setAttribute("disabled", ""); + } else { + this.inputRoomKey = ""; + this.$inputRoomKeyChars.forEach(el => { + this.inputRoomKey += el.value; + }) + this.$submitBtn.removeAttribute("disabled"); + if (document.activeElement === this.$inputRoomKeyChars[5]) { + this._pairDeviceJoin(this.inputRoomKey); + } + } + } + + evaluateUrlAttributes() { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('room_key')) { + this._pairDeviceJoin(urlParams.get('room_key')); + window.history.replaceState({}, "title**", '/'); //remove room_key from url + } + } + + _onWsConnected() { + this.$pairDeviceBtn.removeAttribute('hidden'); + this._evaluateNumberRoomSecrets(); + } + + _pairDeviceInitiate() { + Events.fire('pair-device-initiate'); + } + + _pairDeviceInitiated(msg) { + this.roomKey = msg.roomKey; + this.roomSecret = msg.roomSecret; + this.$roomKey.innerText = `${this.roomKey.substring(0,3)} ${this.roomKey.substring(3,6)}` + // Display the QR code for the url + const qr = new QRCode({ + content: this._getShareRoomURL(), + width: 150, + height: 150, + padding: 0, + background: "transparent", + color: `rgb(var(--text-color))`, + ecl: "L", + join: true + }); + this.$qrCode.innerHTML = qr.svg(); + this.$inputRoomKeyChars.forEach(el => el.removeAttribute("disabled")); + this.show(); + } + + _getShareRoomURL() { + let url = new URL(location.href); + url.searchParams.append('room_key', this.roomKey) + return url.href; + } + + _onSubmit(e) { + e.preventDefault(); + this._pairDeviceJoin(this.inputRoomKey); + } + + _pairDeviceJoin(roomKey) { + if (/^\d{6}$/g.test(roomKey)) { + roomKey = roomKey.substring(0,6); + Events.fire('pair-device-join', roomKey); + let lastChar = this.$inputRoomKeyChars[5]; + lastChar.focus(); + } + } + + _pairDeviceJoined(peerId, roomSecret) { + // skip adding to IndexedDB if peer is another tab on the same browser + if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { + this._cleanUp(); + this.hide(); + Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); + return; + } + + // save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret + this.pairPeer = { + "peerId": peerId, + "roomSecret": roomSecret + }; + } + + _onPeers(message) { + if (!Object.keys(this.pairPeer)) return; + + message.peers.forEach(messagePeer => { + this._evaluateJoinedPeer(messagePeer.id, message.roomSecret); + }); + } + + _onPeerJoined(message) { + if (!Object.keys(this.pairPeer)) return; + + this._evaluateJoinedPeer(message.peer.id, message.roomSecret); + } + + _evaluateJoinedPeer(peerId, roomSecret) { + const samePeerId = peerId === this.pairPeer.peerId; + const sameRoomSecret = roomSecret === this.pairPeer.roomSecret; + + if (!peerId || !roomSecret || !samePeerId || !sameRoomSecret) return; + + this._onPairPeerJoined(peerId, roomSecret); + this.pairPeer = {}; + } + + _onPairPeerJoined(peerId, roomSecret) { + // if devices are paired that are already connected we must save the names at this point + const $peer = $(peerId); + let displayName, deviceName; + if ($peer) { + displayName = $peer.ui._peer.name.displayName; + deviceName = $peer.ui._peer.name.deviceName; + } + + PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) + .then(_ => { + Events.fire('notify-user', 'Devices paired successfully.'); + this._evaluateNumberRoomSecrets(); + }) + .finally(_ => { + this._cleanUp(); + this.hide(); + }) + .catch(_ => { + Events.fire('notify-user', 'Paired devices are not persistent.'); + PersistentStorage.logBrowserNotCapable(); + }); + } + + _pairDeviceJoinKeyInvalid() { + Events.fire('notify-user', 'Key not valid'); + } + + _pairDeviceCancel() { + this.hide(); + this._cleanUp(); + Events.fire('pair-device-cancel'); + } + + _pairDeviceCanceled(roomKey) { + Events.fire('notify-user', `Key ${roomKey} invalidated.`); + } + + _cleanUp() { + this.roomSecret = null; + this.roomKey = null; + this.inputRoomKey = ''; + this.$inputRoomKeyChars.forEach(el => el.value = ''); + this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", "")); + this.pairPeer = {}; + } + + _onSecretRoomDeleted(roomSecret) { + PersistentStorage.deleteRoomSecret(roomSecret).then(_ => { + this._evaluateNumberRoomSecrets(); + }); + } + + _evaluateNumberRoomSecrets() { + PersistentStorage.getAllRoomSecrets().then(roomSecrets => { + if (roomSecrets.length > 0) { + this.$editPairedDevicesBtn.removeAttribute('hidden'); + this.$footerInstructionsPairedDevices.removeAttribute('hidden'); + } else { + this.$editPairedDevicesBtn.setAttribute('hidden', ''); + this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); + } + Events.fire('bg-resize'); + }); + } +} + +class EditPairedDevicesDialog extends Dialog { + constructor() { + super('edit-paired-devices-dialog'); + this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper'); + $('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices()); + + Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e)); + Events.on('keydown', e => this._onKeyDown(e)); + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this.hide(); + } + } + + async _initDOM() { + const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries(); + roomSecretsEntries.forEach(roomSecretsEntry => { + let $pairedDevice = document.createElement('div'); + $pairedDevice.classList = ["paired-device"]; + + $pairedDevice.innerHTML = ` +
+ ${roomSecretsEntry.display_name} +
+
+ ${roomSecretsEntry.device_name} +
+
+ + +
` + + $pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => { + PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => { + Events.fire('auto-accept-updated', { + 'roomSecret': roomSecretsEntry.entry.secret, + 'autoAccept': e.target.checked + }); + }); + }); + + $pairedDevice.querySelector('button').addEventListener('click', e => { + PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => { + Events.fire('room-secrets-deleted', [roomSecret]); + Events.fire('evaluate-number-room-secrets'); + e.target.parentNode.parentNode.remove(); + }); + }) + + this.$pairedDevicesWrapper.appendChild($pairedDevice) + }) + + } + + hide() { + super.hide(); + setTimeout(_ => { + this.$pairedDevicesWrapper.innerHTML = "" + }, 300); + } + + _onEditPairedDevices() { + this._initDOM().then(_ => this.show()); + } + + _clearRoomSecrets() { + PersistentStorage.getAllRoomSecrets() + .then(roomSecrets => { + PersistentStorage.clearRoomSecrets().finally(_ => { + Events.fire('room-secrets-deleted', roomSecrets); + Events.fire('evaluate-number-room-secrets'); + Events.fire('notify-user', 'All Devices unpaired.'); + this.hide(); + }) + }); + } + + _onPeerDisplayNameChanged(e) { + const peerId = e.detail.peerId; + const peerNode = $(peerId); + + if (!peerNode) return; + + const peer = peerNode.ui._peer; + + if (!peer.roomSecret) return; + + PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => { + console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`); + }) + } +} + +class SendTextDialog extends Dialog { + constructor() { + super('send-text-dialog'); + Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); + this.$text = this.$el.querySelector('#text-input'); + this.$peerDisplayName = this.$el.querySelector('.display-name'); + this.$form = this.$el.querySelector('form'); + this.$submit = this.$el.querySelector('button[type="submit"]'); + this.$form.addEventListener('submit', e => this._onSubmit(e)); + this.$text.addEventListener('input', e => this._onChange(e)); + Events.on("keydown", e => this._onKeyDown(e)); + } + + async _onKeyDown(e) { + if (this.isShown()) { + if (e.code === "Escape") { + this.hide(); + } else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) { + if (this._textInputEmpty()) return; + this._send(); + } + } + } + + _textInputEmpty() { + return !this.$text.innerText || this.$text.innerText === "\n"; + } + + _onChange(e) { + if (this._textInputEmpty()) { + this.$submit.setAttribute('disabled', ''); + } else { + this.$submit.removeAttribute('disabled'); + } + } + + _onRecipient(peerId, deviceName) { + this.correspondingPeerId = peerId; + this.$peerDisplayName.innerText = deviceName; + this.show(); + + const range = document.createRange(); + const sel = window.getSelection(); + + this.$text.focus(); + range.selectNodeContents(this.$text); + sel.removeAllRanges(); + sel.addRange(range); + } + + _onSubmit(e) { + e.preventDefault(); + this._send(); + } + + _send() { + Events.fire('send-text', { + to: this.correspondingPeerId, + text: this.$text.innerText + }); + this.$text.value = ""; + this.hide(); + } +} + +class ReceiveTextDialog extends Dialog { + constructor() { + super('receive-text-dialog'); + Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId)); + this.$text = this.$el.querySelector('#text'); + this.$copy = this.$el.querySelector('#copy'); + this.$close = this.$el.querySelector('#close'); + + this.$copy.addEventListener('click', _ => this._onCopy()); + this.$close.addEventListener('click', _ => this.hide()); + + Events.on("keydown", e => this._onKeyDown(e)); + + this.$displayNameNode = this.$el.querySelector('.display-name'); + this._receiveTextQueue = []; + } + + async _onKeyDown(e) { + if (this.isShown()) { + if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) { + await this._onCopy() + this.hide(); + } else if (e.code === "Escape") { + this.hide(); + } + } + } + + _onText(text, peerId) { + window.blop.play(); + this._receiveTextQueue.push({text: text, peerId: peerId}); + this._setDocumentTitleMessages(); + if (this.isShown()) return; + this._dequeueRequests(); + } + + _dequeueRequests() { + if (!this._receiveTextQueue.length) return; + let {text, peerId} = this._receiveTextQueue.shift(); + this._showReceiveTextDialog(text, peerId); + } + + _showReceiveTextDialog(text, peerId) { + this.$displayNameNode.innerText = $(peerId).ui._displayName(); + + this.$text.innerText = text; + this.$text.classList.remove('text-center'); + + // Beautify text if text is short + if (text.length < 2000) { + // replace urls with actual links + this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => { + return `${url}`; + }); + + if (!/\s/.test(text)) { + this.$text.classList.add('text-center'); + } + } + + this._setDocumentTitleMessages(); + + document.changeFavicon("images/favicon-96x96-notification.png"); + this.show(); + } + + _setDocumentTitleMessages() { + document.title = !this._receiveTextQueue.length + ? 'Message Received - MoreLoad' + : `${this._receiveTextQueue.length + 1} Messages Received - MoreLoad`; + } + + async _onCopy() { + await navigator.clipboard.writeText(this.$text.textContent); + Events.fire('notify-user', 'Copied to clipboard'); + this.hide(); + } + + hide() { + super.hide(); + setTimeout(_ => this._dequeueRequests(), 500); + } +} + +class Base64ZipDialog extends Dialog { + + constructor() { + super('base64-paste-dialog'); + const urlParams = new URL(window.location).searchParams; + const base64Text = urlParams.get('base64text'); + const base64Zip = urlParams.get('base64zip'); + const base64Hash = window.location.hash.substring(1); + + this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); + this.$fallbackTextarea = this.$el.querySelector('.textarea'); + + if (base64Text) { + this.show(); + if (base64Text === "paste") { + // ?base64text=paste + // base64 encoded string is ready to be pasted from clipboard + this.preparePasting("text"); + } else if (base64Text === "hash") { + // ?base64text=hash#BASE64ENCODED + // base64 encoded string is url hash which is never sent to server and faster (recommended) + this.processBase64Text(base64Hash) + .catch(_ => { + Events.fire('notify-user', 'Text content is incorrect.'); + console.log("Text content incorrect."); + }).finally(_ => { + this.hide(); + }); + } else { + // ?base64text=BASE64ENCODED + // base64 encoded string was part of url param (not recommended) + this.processBase64Text(base64Text) + .catch(_ => { + Events.fire('notify-user', 'Text content is incorrect.'); + console.log("Text content incorrect."); + }).finally(_ => { + this.hide(); + }); + } + } else if (base64Zip) { + this.show(); + if (base64Zip === "hash") { + // ?base64zip=hash#BASE64ENCODED + // base64 encoded zip file is url hash which is never sent to the server + this.processBase64Zip(base64Hash) + .catch(_ => { + Events.fire('notify-user', 'File content is incorrect.'); + console.log("File content incorrect."); + }).finally(_ => { + this.hide(); + }); + } else { + // ?base64zip=paste || ?base64zip=true + this.preparePasting('files'); + } + } + } + + _setPasteBtnToProcessing() { + this.$pasteBtn.style.pointerEvents = "none"; + this.$pasteBtn.innerText = "Processing..."; + } + + preparePasting(type) { + if (navigator.clipboard.readText) { + this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this._clickCallback = _ => this.processClipboard(type); + this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); + } else { + console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") + this.$pasteBtn.setAttribute('hidden', ''); + this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.removeAttribute('hidden'); + this._inputCallback = _ => this.processInput(type); + this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); + this.$fallbackTextarea.focus(); + } + } + + async processInput(type) { + const base64 = this.$fallbackTextarea.textContent; + this.$fallbackTextarea.textContent = ''; + await this.processBase64(type, base64); + } + + async processClipboard(type) { + const base64 = await navigator.clipboard.readText(); + await this.processBase64(type, base64); + } + + isValidBase64(base64) { + try { + // check if input is base64 encoded + window.atob(base64); + return true; + } catch (e) { + // input is not base64 string. + return false; + } + } + + async processBase64(type, base64) { + if (!base64 || !this.isValidBase64(base64)) return; + this._setPasteBtnToProcessing(); + try { + if (type === "text") { + await this.processBase64Text(base64); + } else { + await this.processBase64Zip(base64); + } + } catch(_) { + Events.fire('notify-user', 'Clipboard content is incorrect.'); + console.log("Clipboard content is incorrect.") + } + this.hide(); + } + + processBase64Text(base64Text){ + return new Promise((resolve) => { + this._setPasteBtnToProcessing(); + let decodedText = decodeURIComponent(escape(window.atob(base64Text))); + Events.fire('activate-paste-mode', {files: [], text: decodedText}); + resolve(); + }); + } + + async processBase64Zip(base64zip) { + this._setPasteBtnToProcessing(); + let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + const zipBlob = new File([u8arr], 'archive.zip'); + + let files = []; + const zipEntries = await zipper.getEntries(zipBlob); + for (let i = 0; i < zipEntries.length; i++) { + let fileBlob = await zipper.getData(zipEntries[i]); + files.push(new File([fileBlob], zipEntries[i].filename)); + } + Events.fire('activate-paste-mode', {files: files, text: ""}); + } + + clearBrowserHistory() { + window.history.replaceState({}, "Rewrite URL", '/'); + } + + hide() { + this.clearBrowserHistory(); + this.$pasteBtn.removeEventListener('click', _ => this._clickCallback()); + this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback()); + super.hide(); + } +} + +class Toast extends Dialog { + constructor() { + super('toast'); + Events.on('notify-user', e => this._onNotify(e.detail)); + } + + _onNotify(message) { + if (this.hideTimeout) clearTimeout(this.hideTimeout); + this.$el.textContent = message; + this.show(); + this.hideTimeout = setTimeout(_ => this.hide(), 5000); + } +} + +class Notifications { + + constructor() { + // Check if the browser supports notifications + if (!('Notification' in window)) return; + + // Check whether notification permissions have already been granted + if (Notification.permission !== 'granted') { + this.$button = $('notification'); + this.$button.removeAttribute('hidden'); + this.$button.addEventListener('click', _ => this._requestPermission()); + } + Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); + Events.on('files-received', e => this._downloadNotification(e.detail.files)); + Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId)); + } + + _requestPermission() { + Notification.requestPermission(permission => { + if (permission !== 'granted') { + Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); + return; + } + Events.fire('notify-user', 'Notifications enabled.'); + this.$button.setAttribute('hidden', 1); + }); + } + + _notify(title, body) { + const config = { + body: body, + icon: '/images/logo_transparent_128x128.png', + } + let notification; + try { + notification = new Notification(title, config); + } catch (e) { + // Android doesn't support "new Notification" if service worker is installed + if (!serviceWorker || !serviceWorker.showNotification) return; + notification = serviceWorker.showNotification(title, config); + } + + // Notification is persistent on Android. We have to close it manually + const visibilitychangeHandler = () => { + if (document.visibilityState === 'visible') { + notification.close(); + Events.off('visibilitychange', visibilitychangeHandler); + } + }; + Events.on('visibilitychange', visibilitychangeHandler); + + return notification; + } + + _messageNotification(message, peerId) { + if (document.visibilityState !== 'visible') { + const peerDisplayName = $(peerId).ui._displayName(); + if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { + const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); + this._bind(notification, _ => window.open(message, '_blank', null, true)); + } else { + const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); + this._bind(notification, _ => this._copyText(message, notification)); + } + } + } + + _downloadNotification(files) { + if (document.visibilityState !== 'visible') { + let imagesOnly = true; + for(let i=0; i= 2) { + title += ` and ${files.length - 1} other `; + title += imagesOnly ? 'image' : 'file'; + if (files.length > 2) title += "s"; + } + const notification = this._notify(title, 'Click to download'); + this._bind(notification, _ => this._download(notification)); + } + } + + _requestNotification(request, peerId) { + if (document.visibilityState !== 'visible') { + let imagesOnly = true; + for(let i=0; i 1) { + descriptor = imagesOnly ? ' images' : ' files'; + } else { + descriptor = imagesOnly ? ' image' : ' file'; + } + let displayName = $(peerId).querySelector('.name').textContent + let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`; + const notification = this._notify(title, 'Click to show'); + } + } + + _download(notification) { + $('download-btn').click(); + notification.close(); + } + + _copyText(message, notification) { + if (navigator.clipboard.writeText(message)) { + notification.close(); + this._notify('Copied text to clipboard'); + } else { + this._notify('Writing to clipboard failed. Copy manually!'); + + } + } + + _bind(notification, handler) { + if (notification.then) { + notification.then(_ => serviceWorker.getNotifications().then(_ => { + serviceWorker.addEventListener('notificationclick', handler); + })); + } else { + notification.onclick = handler; + } + } +} + +class NetworkStatusUI { + + constructor() { + Events.on('offline', _ => this._showOfflineMessage()); + Events.on('online', _ => this._showOnlineMessage()); + if (!navigator.onLine) this._showOfflineMessage(); + } + + _showOfflineMessage() { + Events.fire('notify-user', 'You are offline'); + } + + _showOnlineMessage() { + Events.fire('notify-user', 'You are back online'); + } +} + +class WebShareTargetUI { + constructor() { + const urlParams = new URL(window.location).searchParams; + const share_target_type = urlParams.get("share-target") + if (share_target_type) { + if (share_target_type === "text") { + const title = urlParams.get('title') || ''; + const text = urlParams.get('text') || ''; + const url = urlParams.get('url') || ''; + let shareTargetText; + + if (url) { + shareTargetText = url; // we share only the link - no text. + } else if (title && text) { + shareTargetText = title + '\r\n' + text; + } else { + shareTargetText = title + text; + } + + Events.fire('activate-paste-mode', {files: [], text: shareTargetText}) + } else if (share_target_type === "files") { + let openRequest = window.indexedDB.open('moreload_store') + openRequest.onsuccess = e => { + const db = e.target.result; + const tx = db.transaction('share_target_files', 'readwrite'); + const store = tx.objectStore('share_target_files'); + const request = store.getAll(); + request.onsuccess = _ => { + const fileObjects = request.result; + let filesReceived = []; + for (let i=0; i db.close(); + + Events.fire('activate-paste-mode', {files: filesReceived, text: ""}) + } + } + } + window.history.replaceState({}, "Rewrite URL", '/'); + } + } +} + +class WebFileHandlersUI { + constructor() { + const urlParams = new URL(window.location).searchParams; + if (urlParams.has("file_handler") && "launchQueue" in window) { + launchQueue.setConsumer(async launchParams => { + console.log("Launched with: ", launchParams); + if (!launchParams.files.length) + return; + let files = []; + + for (let i=0; i NoSleepUI.disable(), 10000); + } + } + + static disable() { + if ($$('x-peer[status]') === null) { + clearInterval(NoSleepUI._interval); + NoSleepUI._nosleep.disable(); + } + } +} + +class PersistentStorage { + constructor() { + if (!('indexedDB' in window)) { + PersistentStorage.logBrowserNotCapable(); + return; + } + const DBOpenRequest = window.indexedDB.open('moreload_store', 4); + DBOpenRequest.onerror = (e) => { + PersistentStorage.logBrowserNotCapable(); + console.log('Error initializing database: '); + console.log(e) + }; + DBOpenRequest.onsuccess = () => { + console.log('Database initialised.'); + }; + DBOpenRequest.onupgradeneeded = (e) => { + const db = e.target.result; + const txn = e.target.transaction; + + db.onerror = e => console.log('Error loading database: ' + e); + + console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`); + + if (e.oldVersion === 0) { + // initiate v1 + db.createObjectStore('keyval'); + let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true}); + roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true }); + } + if (e.oldVersion <= 1) { + // migrate to v2 + db.createObjectStore('share_target_files'); + } + if (e.oldVersion <= 2) { + // migrate to v3 + db.deleteObjectStore('share_target_files'); + db.createObjectStore('share_target_files', {autoIncrement: true}); + } + if (e.oldVersion <= 3) { + // migrate to v4 + let roomSecretsObjectStore4 = txn.objectStore('room_secrets'); + roomSecretsObjectStore4.createIndex('display_name', 'display_name'); + roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept'); + } + } + } + + static logBrowserNotCapable() { + console.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed."); + } + + static set(key, value) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('keyval', 'readwrite'); + const objectStore = transaction.objectStore('keyval'); + const objectStoreRequest = objectStore.put(value, key); + objectStoreRequest.onsuccess = _ => { + console.log(`Request successful. Added key-pair: ${key} - ${value}`); + resolve(value); + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static get(key) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('keyval', 'readonly'); + const objectStore = transaction.objectStore('keyval'); + const objectStoreRequest = objectStore.get(key); + objectStoreRequest.onsuccess = _ => { + console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`); + resolve(objectStoreRequest.result); + } + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }); + } + + static delete(key) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('keyval', 'readwrite'); + const objectStore = transaction.objectStore('keyval'); + const objectStoreRequest = objectStore.delete(key); + objectStoreRequest.onsuccess = _ => { + console.log(`Request successful. Deleted key: ${key}`); + resolve(); + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static addRoomSecret(roomSecret, displayName, deviceName) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequest = objectStore.add({ + 'secret': roomSecret, + 'display_name': displayName, + 'device_name': deviceName, + 'auto_accept': false + }); + objectStoreRequest.onsuccess = e => { + console.log(`Request successful. RoomSecret added: ${e.target.result}`); + resolve(); + } + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static async getAllRoomSecrets() { + try { + const roomSecrets = await this.getAllRoomSecretEntries(); + let secrets = []; + for (let i = 0; i < roomSecrets.length; i++) { + secrets.push(roomSecrets[i].secret); + } + console.log(`Request successful. Retrieved ${secrets.length} room_secrets`); + return(secrets); + } catch (e) { + this.logBrowserNotCapable(); + return false; + } + } + + static getAllRoomSecretEntries() { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readonly'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequest = objectStore.getAll(); + objectStoreRequest.onsuccess = e => { + resolve(e.target.result); + } + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }); + } + + static getRoomSecretEntry(roomSecret) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readonly'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret); + objectStoreRequestKey.onsuccess = e => { + const key = e.target.result; + if (!key) { + console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`); + resolve(); + return; + } + const objectStoreRequestRetrieval = objectStore.get(key); + objectStoreRequestRetrieval.onsuccess = e => { + console.log(`Request successful. Retrieved entry for room_secret: ${key}`); + resolve({ + "entry": e.target.result, + "key": key + }); + } + objectStoreRequestRetrieval.onerror = (e) => { + reject(e); + } + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }); + } + + static deleteRoomSecret(roomSecret) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret); + objectStoreRequestKey.onsuccess = e => { + if (!e.target.result) { + console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`); + resolve(); + return; + } + const key = e.target.result; + const objectStoreRequestDeletion = objectStore.delete(key); + objectStoreRequestDeletion.onsuccess = _ => { + console.log(`Request successful. Deleted room_secret: ${key}`); + resolve(roomSecret); + } + objectStoreRequestDeletion.onerror = (e) => { + reject(e); + } + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static clearRoomSecrets() { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequest = objectStore.clear(); + objectStoreRequest.onsuccess = _ => { + console.log('Request successful. All room_secrets cleared'); + resolve(); + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static updateRoomSecretNames(roomSecret, displayName, deviceName) { + return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName); + } + + static updateRoomSecretAutoAccept(roomSecret, autoAccept) { + return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept); + } + + static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + this.getRoomSecretEntry(roomSecret) + .then(roomSecretEntry => { + if (!roomSecretEntry) { + resolve(false); + return; + } + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + // Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers + const updatedRoomSecretEntry = { + 'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret, + 'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name, + 'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name, + 'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept + }; + + const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key); + + objectStoreRequestUpdate.onsuccess = e => { + console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`); + resolve({ + "entry": updatedRoomSecretEntry, + "key": roomSecretEntry.key + }); + } + + objectStoreRequestUpdate.onerror = (e) => { + reject(e); + } + }) + .catch(e => reject(e)); + }; + + DBOpenRequest.onerror = e => reject(e); + }) + } +} + +class BrowserTabsConnector { + constructor() { + this.bc = new BroadcastChannel('moreload'); + this.bc.addEventListener('message', e => this._onMessage(e)); + Events.on('broadcast-send', e => this._broadcastSend(e.detail)); + } + + _broadcastSend(message) { + this.bc.postMessage(message); + } + + _onMessage(e) { + console.log('Broadcast:', e.data) + switch (e.data.type) { + case 'self-display-name-changed': + Events.fire('self-display-name-changed', e.data.detail); + break; + } + } + + static peerIsSameBrowser(peerId) { + let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser")); + return peerIdsBrowser + ? peerIdsBrowser.indexOf(peerId) !== -1 + : false; + } + + static async addPeerIdToLocalStorage() { + const peerId = sessionStorage.getItem("peerId"); + if (!peerId) return false; + + let peerIdsBrowser = []; + let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser")); + + if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld); + peerIdsBrowser.push(peerId); + peerIdsBrowser = peerIdsBrowser.filter(onlyUnique); + localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser)); + + return peerIdsBrowser; + } + + static async removePeerIdFromLocalStorage(peerId) { + let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser")); + const index = peerIdsBrowser.indexOf(peerId); + peerIdsBrowser.splice(index, 1); + localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser)); + return peerId; + } + + + static async removeOtherPeerIdsFromLocalStorage() { + const peerId = sessionStorage.getItem("peerId"); + if (!peerId) return false; + + let peerIdsBrowser = [peerId]; + localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser)); + return peerIdsBrowser; + } +} + +class MoreLoad { + constructor() { + Events.on('load', _ => { + const server = new ServerConnection(); + const peers = new PeersManager(server); + const peersUI = new PeersUI(); + const receiveFileDialog = new ReceiveFileDialog(); + const receiveRequestDialog = new ReceiveRequestDialog(); + const sendTextDialog = new SendTextDialog(); + const receiveTextDialog = new ReceiveTextDialog(); + const pairDeviceDialog = new PairDeviceDialog(); + const clearDevicesDialog = new EditPairedDevicesDialog(); + const base64ZipDialog = new Base64ZipDialog(); + const toast = new Toast(); + const notifications = new Notifications(); + const networkStatusUI = new NetworkStatusUI(); + const webShareTargetUI = new WebShareTargetUI(); + const webFileHandlersUI = new WebFileHandlersUI(); + const noSleepUI = new NoSleepUI(); + const broadCast = new BrowserTabsConnector(); + }); + } +} + +const persistentStorage = new PersistentStorage(); +const moreload = new MoreLoad(); + + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/service-worker.js') + .then(serviceWorker => { + console.log('Service Worker registered'); + window.serviceWorker = serviceWorker + }); +} + +window.addEventListener('beforeinstallprompt', e => { + if (!window.matchMedia('(display-mode: minimal-ui)').matches) { + // only display install btn when installed + const btn = document.querySelector('#install') + btn.hidden = false; + btn.onclick = _ => e.prompt(); + } + return e.preventDefault(); +}); + +// Background Circles +Events.on('load', () => { + let c = $$('canvas'); + let cCtx = c.getContext('2d'); + let x0, y0, w, h, dw, offset; + + function init() { + let oldW = w; + let oldH = h; + let oldOffset = offset + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; + offset = $$('footer').offsetHeight - 33; + if (h > 800) offset += 16; + + if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed + + c.width = w; + c.height = h; + x0 = w / 2; + y0 = h - offset; + dw = Math.round(Math.max(w, h, 1000) / 13); + + drawCircles(cCtx, dw); + } + + Events.on('bg-resize', _ => init()); + window.onresize = _ => Events.fire('bg-resize'); + + function drawCircle(ctx, radius) { + ctx.beginPath(); + ctx.lineWidth = 2; + let opacity = 0.3 * (1 - 1.2 * radius / Math.max(w, h)); + ctx.strokeStyle = `rgba(128, 128, 128, ${opacity})`; + ctx.arc(x0, y0, radius, 0, 2 * Math.PI); + ctx.stroke(); + } + + function drawCircles(ctx, frame) { + ctx.clearRect(0, 0, w, h); + for (let i = 0; i < 13; i++) { + drawCircle(ctx, dw * i + frame + 33); + } + } + + init(); +}); + +document.changeFavicon = function (src) { + document.querySelector('[rel="icon"]').href = src; + document.querySelector('[rel="shortcut icon"]').href = src; +} + +// close About MoreLoad page on Escape +window.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + window.location.hash = '#'; + } +}); + +Notifications.PERMISSION_ERROR = ` +Notifications permission has been blocked +as the user has dismissed the permission prompt several times. +This can be reset in Page Info +which can be accessed by clicking the lock icon next to the URL.`; diff --git a/public/scripts/util.js b/public/scripts/util.js new file mode 100644 index 0000000..bae1e39 --- /dev/null +++ b/public/scripts/util.js @@ -0,0 +1,404 @@ +// Polyfill for Navigator.clipboard.writeText +if (!navigator.clipboard) { + navigator.clipboard = { + writeText: text => { + + // A contains the text to copy + const span = document.createElement('span'); + span.textContent = text; + span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines + + // Paint the span outside the viewport + span.style.position = 'absolute'; + span.style.left = '-9999px'; + span.style.top = '-9999px'; + + const win = window; + const selection = win.getSelection(); + win.document.body.appendChild(span); + + const range = win.document.createRange(); + selection.removeAllRanges(); + range.selectNode(span); + selection.addRange(range); + + let success = false; + try { + success = win.document.execCommand('copy'); + } catch (err) { + return Promise.error(); + } + + selection.removeAllRanges(); + span.remove(); + + return Promise.resolve(); + } + } +} + +const zipper = (() => { + + let zipWriter; + return { + createNewZipWriter() { + zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, level: 0 }); + }, + addFile(file, options) { + return zipWriter.add(file.name, new zip.BlobReader(file), options); + }, + async getBlobURL() { + if (zipWriter) { + const blobURL = URL.createObjectURL(await zipWriter.close()); + zipWriter = null; + return blobURL; + } else { + throw new Error("Zip file closed"); + } + }, + async getZipFile(filename = "archive.zip") { + if (zipWriter) { + const file = new File([await zipWriter.close()], filename, {type: "application/zip"}); + zipWriter = null; + return file; + } else { + throw new Error("Zip file closed"); + } + }, + async getEntries(file, options) { + return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options); + }, + async getData(entry, options) { + return await entry.getData(new zip.BlobWriter(), options); + }, + }; + +})(); + +const mime = (() => { + + return { + getMimeByFilename(filename) { + try { + const arr = filename.split('.'); + const suffix = arr[arr.length - 1].toLowerCase(); + return { + "cpl": "application/cpl+xml", + "gpx": "application/gpx+xml", + "gz": "application/gzip", + "jar": "application/java-archive", + "war": "application/java-archive", + "ear": "application/java-archive", + "class": "application/java-vm", + "js": "application/javascript", + "mjs": "application/javascript", + "json": "application/json", + "map": "application/json", + "webmanifest": "application/manifest+json", + "doc": "application/msword", + "dot": "application/msword", + "wiz": "application/msword", + "bin": "application/octet-stream", + "dms": "application/octet-stream", + "lrf": "application/octet-stream", + "mar": "application/octet-stream", + "so": "application/octet-stream", + "dist": "application/octet-stream", + "distz": "application/octet-stream", + "pkg": "application/octet-stream", + "bpk": "application/octet-stream", + "dump": "application/octet-stream", + "elc": "application/octet-stream", + "deploy": "application/octet-stream", + "img": "application/octet-stream", + "msp": "application/octet-stream", + "msm": "application/octet-stream", + "buffer": "application/octet-stream", + "oda": "application/oda", + "oxps": "application/oxps", + "pdf": "application/pdf", + "asc": "application/pgp-signature", + "sig": "application/pgp-signature", + "prf": "application/pics-rules", + "p7c": "application/pkcs7-mime", + "cer": "application/pkix-cert", + "ai": "application/postscript", + "eps": "application/postscript", + "ps": "application/postscript", + "apk": "application/vnd.android.package-archive", + "m3u8": "application/vnd.apple.mpegurl", + "pkpass": "application/vnd.apple.pkpass", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "cab": "application/vnd.ms-cab-compressed", + "xls": "application/vnd.ms-excel", + "xlm": "application/vnd.ms-excel", + "xla": "application/vnd.ms-excel", + "xlc": "application/vnd.ms-excel", + "xlt": "application/vnd.ms-excel", + "xlw": "application/vnd.ms-excel", + "msg": "application/vnd.ms-outlook", + "ppt": "application/vnd.ms-powerpoint", + "pot": "application/vnd.ms-powerpoint", + "ppa": "application/vnd.ms-powerpoint", + "pps": "application/vnd.ms-powerpoint", + "pwz": "application/vnd.ms-powerpoint", + "mpp": "application/vnd.ms-project", + "mpt": "application/vnd.ms-project", + "xps": "application/vnd.ms-xpsdocument", + "odb": "application/vnd.oasis.opendocument.database", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "odt": "application/vnd.oasis.opendocument.text", + "osm": "application/vnd.openstreetmap.data+xml", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "pcap": "application/vnd.tcpdump.pcap", + "cap": "application/vnd.tcpdump.pcap", + "dmp": "application/vnd.tcpdump.pcap", + "wpd": "application/vnd.wordperfect", + "wasm": "application/wasm", + "7z": "application/x-7z-compressed", + "dmg": "application/x-apple-diskimage", + "bcpio": "application/x-bcpio", + "torrent": "application/x-bittorrent", + "cbr": "application/x-cbr", + "cba": "application/x-cbr", + "cbt": "application/x-cbr", + "cbz": "application/x-cbr", + "cb7": "application/x-cbr", + "vcd": "application/x-cdlink", + "crx": "application/x-chrome-extension", + "cpio": "application/x-cpio", + "csh": "application/x-csh", + "deb": "application/x-debian-package", + "udeb": "application/x-debian-package", + "dvi": "application/x-dvi", + "arc": "application/x-freearc", + "gtar": "application/x-gtar", + "hdf": "application/x-hdf", + "h5": "application/x-hdf5", + "php": "application/x-httpd-php", + "iso": "application/x-iso9660-image", + "key": "application/x-iwork-keynote-sffkey", + "numbers": "application/x-iwork-numbers-sffnumbers", + "pages": "application/x-iwork-pages-sffpages", + "latex": "application/x-latex", + "run": "application/x-makeself", + "mif": "application/x-mif", + "lnk": "application/x-ms-shortcut", + "mdb": "application/x-msaccess", + "exe": "application/x-msdownload", + "dll": "application/x-msdownload", + "com": "application/x-msdownload", + "bat": "application/x-msdownload", + "msi": "application/x-msdownload", + "pub": "application/x-mspublisher", + "cdf": "application/x-netcdf", + "nc": "application/x-netcdf", + "pl": "application/x-perl", + "pm": "application/x-perl", + "prc": "application/x-pilot", + "pdb": "application/x-pilot", + "p12": "application/x-pkcs12", + "pfx": "application/x-pkcs12", + "ram": "application/x-pn-realaudio", + "pyc": "application/x-python-code", + "pyo": "application/x-python-code", + "rar": "application/x-rar-compressed", + "rpm": "application/x-redhat-package-manager", + "sh": "application/x-sh", + "shar": "application/x-shar", + "swf": "application/x-shockwave-flash", + "sql": "application/x-sql", + "srt": "application/x-subrip", + "sv4cpio": "application/x-sv4cpio", + "sv4crc": "application/x-sv4crc", + "gam": "application/x-tads", + "tar": "application/x-tar", + "tcl": "application/x-tcl", + "tex": "application/x-tex", + "roff": "application/x-troff", + "t": "application/x-troff", + "tr": "application/x-troff", + "man": "application/x-troff-man", + "me": "application/x-troff-me", + "ms": "application/x-troff-ms", + "ustar": "application/x-ustar", + "src": "application/x-wais-source", + "xpi": "application/x-xpinstall", + "xhtml": "application/xhtml+xml", + "xht": "application/xhtml+xml", + "xsl": "application/xml", + "rdf": "application/xml", + "wsdl": "application/xml", + "xpdl": "application/xml", + "zip": "application/zip", + "3gp": "audio/3gp", + "3gpp": "audio/3gpp", + "3g2": "audio/3gpp2", + "3gpp2": "audio/3gpp2", + "aac": "audio/aac", + "adts": "audio/aac", + "loas": "audio/aac", + "ass": "audio/aac", + "au": "audio/basic", + "snd": "audio/basic", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "rmi": "audio/midi", + "mpga": "audio/mpeg", + "mp2": "audio/mpeg", + "mp2a": "audio/mpeg", + "mp3": "audio/mpeg", + "m2a": "audio/mpeg", + "m3a": "audio/mpeg", + "oga": "audio/ogg", + "ogg": "audio/ogg", + "spx": "audio/ogg", + "opus": "audio/opus", + "aif": "audio/x-aiff", + "aifc": "audio/x-aiff", + "aiff": "audio/x-aiff", + "flac": "audio/x-flac", + "m4a": "audio/x-m4a", + "m3u": "audio/x-mpegurl", + "wma": "audio/x-ms-wma", + "ra": "audio/x-pn-realaudio", + "wav": "audio/x-wav", + "otf": "font/otf", + "ttf": "font/ttf", + "woff": "font/woff", + "woff2": "font/woff2", + "emf": "image/emf", + "gif": "image/gif", + "heic": "image/heic", + "heif": "image/heif", + "ief": "image/ief", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "pict": "image/pict", + "pct": "image/pict", + "pic": "image/pict", + "png": "image/png", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "psd": "image/vnd.adobe.photoshop", + "djvu": "image/vnd.djvu", + "djv": "image/vnd.djvu", + "dwg": "image/vnd.dwg", + "dxf": "image/vnd.dxf", + "dds": "image/vnd.ms-dds", + "webp": "image/webp", + "3ds": "image/x-3ds", + "ras": "image/x-cmu-raster", + "ico": "image/x-icon", + "bmp": "image/x-ms-bmp", + "pnm": "image/x-portable-anymap", + "pbm": "image/x-portable-bitmap", + "pgm": "image/x-portable-graymap", + "ppm": "image/x-portable-pixmap", + "rgb": "image/x-rgb", + "tga": "image/x-tga", + "xbm": "image/x-xbitmap", + "xpm": "image/x-xpixmap", + "xwd": "image/x-xwindowdump", + "eml": "message/rfc822", + "mht": "message/rfc822", + "mhtml": "message/rfc822", + "nws": "message/rfc822", + "obj": "model/obj", + "stl": "model/stl", + "dae": "model/vnd.collada+xml", + "ics": "text/calendar", + "ifb": "text/calendar", + "css": "text/css", + "csv": "text/csv", + "html": "text/html", + "htm": "text/html", + "shtml": "text/html", + "markdown": "text/markdown", + "md": "text/markdown", + "txt": "text/plain", + "text": "text/plain", + "conf": "text/plain", + "def": "text/plain", + "list": "text/plain", + "log": "text/plain", + "in": "text/plain", + "ini": "text/plain", + "rtx": "text/richtext", + "rtf": "text/rtf", + "tsv": "text/tab-separated-values", + "c": "text/x-c", + "cc": "text/x-c", + "cxx": "text/x-c", + "cpp": "text/x-c", + "h": "text/x-c", + "hh": "text/x-c", + "dic": "text/x-c", + "java": "text/x-java-source", + "lua": "text/x-lua", + "py": "text/x-python", + "etx": "text/x-setext", + "sgm": "text/x-sgml", + "sgml": "text/x-sgml", + "vcf": "text/x-vcard", + "xml": "text/xml", + "xul": "text/xul", + "yaml": "text/yaml", + "yml": "text/yaml", + "ts": "video/mp2t", + "mp4": "video/mp4", + "mp4v": "video/mp4", + "mpg4": "video/mp4", + "mpeg": "video/mpeg", + "m1v": "video/mpeg", + "mpa": "video/mpeg", + "mpe": "video/mpeg", + "mpg": "video/mpeg", + "mov": "video/quicktime", + "qt": "video/quicktime", + "webm": "video/webm", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "asf": "video/x-ms-asf", + "asx": "video/x-ms-asf", + "vob": "video/x-ms-vob", + "wmv": "video/x-ms-wmv", + "avi": "video/x-msvideo", + "*": "video/x-sgi-movie", + }[suffix] || ''; + } catch (e) { + console.error(e); + return ''; + } + } + }; + +})(); + +/* + cyrb53 (c) 2018 bryc (github.com/bryc) + A fast and simple hash function with decent collision resistance. + Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. + Public domain. Attribution appreciated. +*/ +const cyrb53 = function(str, seed = 0) { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909); + h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1>>>0); +}; + +function onlyUnique (value, index, array) { + return array.indexOf(value) === index; +} diff --git a/public/scripts/zip.min.js b/public/scripts/zip.min.js new file mode 100644 index 0000000..b9db6df --- /dev/null +++ b/public/scripts/zip.min.js @@ -0,0 +1 @@ +((e,t)=>{"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).zip={})})(this,(function(e){"use strict";const{Array:t,Object:n,String:r,Number:s,BigInt:i,Math:a,Date:o,Map:c,Set:l,Response:u,URL:f,Error:d,Uint8Array:w,Uint16Array:h,Uint32Array:p,DataView:g,Blob:m,Promise:y,TextEncoder:b,TextDecoder:S,document:k,crypto:z,btoa:x,TransformStream:v,ReadableStream:C,WritableStream:_,CompressionStream:D,DecompressionStream:R,navigator:F,Worker:E}="undefined"!=typeof globalThis?globalThis:this||self,I=4294967295,A=65535,T=67324752,H=134695760,O=33639248,U=101075792,N=117853008,W=21589,q=2048,L="/",M=new o(2107,11,31),P=new o(1980,0,1),V=void 0,B="undefined",K="function";class Y{constructor(e){return class extends v{constructor(t,n){const r=new e(n);super({transform(e,t){t.enqueue(r.append(e))},flush(e){const t=r.flush();t&&e.enqueue(t)}})}}}}let Z=2;try{typeof F!=B&&F.hardwareConcurrency&&(Z=F.hardwareConcurrency)}catch(e){}const X={chunkSize:524288,maxWorkers:Z,terminateWorkerTimeout:5e3,useWebWorkers:!0,useCompressionStream:!0,workerScripts:V,CompressionStreamNative:typeof D!=B&&D,DecompressionStreamNative:typeof R!=B&&R},G=n.assign({},X);function j(){return G}function J(e){return a.max(e.chunkSize,64)}function Q(e){const{baseURL:n,chunkSize:r,maxWorkers:s,terminateWorkerTimeout:i,useCompressionStream:a,useWebWorkers:o,Deflate:c,Inflate:l,CompressionStream:u,DecompressionStream:f,workerScripts:w}=e;if($("baseURL",n),$("chunkSize",r),$("maxWorkers",s),$("terminateWorkerTimeout",i),$("useCompressionStream",a),$("useWebWorkers",o),c&&(G.CompressionStream=new Y(c)),l&&(G.DecompressionStream=new Y(l)),$("CompressionStream",u),$("DecompressionStream",f),w!==V){const{deflate:e,inflate:n}=w;if((e||n)&&(G.workerScripts||(G.workerScripts={})),e){if(!t.isArray(e))throw new d("workerScripts.deflate must be an array");G.workerScripts.deflate=e}if(n){if(!t.isArray(n))throw new d("workerScripts.inflate must be an array");G.workerScripts.inflate=n}}}function $(e,t){t!==V&&(G[e]=t)}function ee(e,t,r){return class{constructor(s){const i=this;n.hasOwn(s,"level")&&void 0===s.level&&delete s.level,i.codec=new e(n.assign({},t,s)),r(i.codec,(e=>{if(i.pendingData){const t=i.pendingData;i.pendingData=new w(t.length+e.length);const{pendingData:n}=i;n.set(t,0),n.set(e,t.length)}else i.pendingData=new w(e)}))}append(e){return this.codec.push(e),s(this)}flush(){return this.codec.push(new w,!0),s(this)}};function s(e){if(e.pendingData){const t=e.pendingData;return e.pendingData=null,t}return new w}}const te=[];for(let e=0;256>e;e++){let t=e;for(let e=0;8>e;e++)1&t?t=t>>>1^3988292384:t>>>=1;te[e]=t}class ne{constructor(e){this.crc=e||-1}append(e){let t=0|this.crc;for(let n=0,r=0|e.length;r>n;n++)t=t>>>8^te[255&(t^e[n])];this.crc=t}get(){return~this.crc}}class re extends v{constructor(){const e=new ne;super({transform(t){e.append(t)},flush(t){const n=new w(4);new g(n.buffer).setUint32(0,e.get()),t.enqueue(n)}})}}function se(e){if(void 0===b){const t=new w((e=unescape(encodeURIComponent(e))).length);for(let n=0;n0&&t&&(e[n-1]=ie.partial(t,e[n-1]&2147483648>>t-1,1)),e},partial:(e,t,n)=>32===e?t:(n?0|t:t<<32-e)+1099511627776*e,getPartial:e=>a.round(e/1099511627776)||32,_shiftRight(e,t,n,r){for(void 0===r&&(r=[]);t>=32;t-=32)r.push(n),n=0;if(0===t)return r.concat(e);for(let s=0;s>>t),n=e[s]<<32-t;const s=e.length?e[e.length-1]:0,i=ie.getPartial(s);return r.push(ie.partial(t+i&31,t+i>32?n:r.pop(),1)),r}},ae={bytes:{fromBits(e){const t=ie.bitLength(e)/8,n=new w(t);let r;for(let s=0;t>s;s++)0==(3&s)&&(r=e[s/4]),n[s]=r>>>24,r<<=8;return n},toBits(e){const t=[];let n,r=0;for(n=0;n{let t=987654321;const n=4294967295;return()=>(t=36969*(65535&t)+(t>>16)&n,(((t<<16)+(e=18e3*(65535&e)+(e>>16)&n)&n)/4294967296+.5)*(a.random()>.5?1:-1))};for(let r,s=0;snew ce.hmacSha1(ae.bytes.toBits(e)),pbkdf2(e,t,n,r){if(n=n||1e4,0>r||0>n)throw new d("invalid params to pbkdf2");const s=1+(r>>5)<<2;let i,a,o,c,l;const u=new ArrayBuffer(s),f=new g(u);let w=0;const h=ie;for(t=ae.bytes.toBits(t),l=1;(s||1)>w;l++){for(i=a=e.encrypt(h.concat(t,[l])),o=1;n>o;o++)for(a=e.encrypt(a),c=0;cw&&o9007199254740991)throw new d("Cannot hash more than 2^53 - 1 bits");const i=new p(n);let a=0;for(let e=t.blockSize+r-(t.blockSize+r&t.blockSize-1);s>=e;e+=t.blockSize)t._block(i.subarray(16*a,16*(a+1))),a+=1;return n.splice(0,16*a),t}finalize(){const e=this;let t=e._buffer;const n=e._h;t=ie.concat(t,[ie.partial(1,1)]);for(let e=t.length+2;15&e;e++)t.push(0);for(t.push(a.floor(e._length/4294967296)),t.push(0|e._length);t.length;)e._block(t.splice(0,16));return e.reset(),n}_f(e,t,n,r){return e>19?e>39?e>59?e>79?void 0:t^n^r:t&n|t&r|n&r:t^n^r:t&n|~t&r}_S(e,t){return t<>>32-e}_block(e){const n=this,r=n._h,s=t(80);for(let t=0;16>t;t++)s[t]=e[t];let i=r[0],o=r[1],c=r[2],l=r[3],u=r[4];for(let e=0;79>=e;e++){16>e||(s[e]=n._S(1,s[e-3]^s[e-8]^s[e-14]^s[e-16]));const t=n._S(5,i)+n._f(e,o,c,l)+u+s[e]+n._key[a.floor(e/20)]|0;u=l,l=c,c=n._S(30,o),o=i,i=t}r[0]=r[0]+i|0,r[1]=r[1]+o|0,r[2]=r[2]+c|0,r[3]=r[3]+l|0,r[4]=r[4]+u|0}},s=[[],[]];n._baseHash=[new r,new r];const i=n._baseHash[0].blockSize/32;e.length>i&&(e=r.hash(e));for(let t=0;i>t;t++)s[0][t]=909522486^e[t],s[1][t]=1549556828^e[t];n._baseHash[0].update(s[0]),n._baseHash[1].update(s[1]),n._resultHash=new r(n._baseHash[0])}reset(){const e=this;e._resultHash=new e._hash(e._baseHash[0]),e._updated=!1}update(e){this._updated=!0,this._resultHash.update(e)}digest(){const e=this,t=e._resultHash.finalize(),n=new e._hash(e._baseHash[1]).update(t).finalize();return e.reset(),n}encrypt(e){if(this._updated)throw new d("encrypt on already updated hmac called!");return this.update(e),this.digest(e)}}},le=void 0!==z&&"function"==typeof z.getRandomValues,ue="Invalid password",fe="Invalid signature";function de(e){return le?z.getRandomValues(e):oe.getRandomValues(e)}const we=16,he={name:"PBKDF2"},pe=n.assign({hash:{name:"HMAC"}},he),ge=n.assign({iterations:1e3,hash:{name:"SHA-1"}},he),me=["deriveBits"],ye=[8,12,16],be=[16,24,32],Se=10,ke=[0,0,0,0],ze="undefined",xe="function",ve=typeof z!=ze,Ce=ve&&z.subtle,_e=ve&&typeof Ce!=ze,De=ae.bytes,Re=class{constructor(e){const t=this;t._tables=[[[],[],[],[],[]],[[],[],[],[],[]]],t._tables[0][0][0]||t._precompute();const n=t._tables[0][4],r=t._tables[1],s=e.length;let i,a,o,c=1;if(4!==s&&6!==s&&8!==s)throw new d("invalid aes key size");for(t._key=[a=e.slice(0),o=[]],i=s;4*s+28>i;i++){let e=a[i-1];(i%s==0||8===s&&i%s==4)&&(e=n[e>>>24]<<24^n[e>>16&255]<<16^n[e>>8&255]<<8^n[255&e],i%s==0&&(e=e<<8^e>>>24^c<<24,c=c<<1^283*(c>>7))),a[i]=a[i-s]^e}for(let e=0;i;e++,i--){const t=a[3&e?i:i-4];o[e]=4>=i||4>e?t:r[0][n[t>>>24]]^r[1][n[t>>16&255]]^r[2][n[t>>8&255]]^r[3][n[255&t]]}}encrypt(e){return this._crypt(e,0)}decrypt(e){return this._crypt(e,1)}_precompute(){const e=this._tables[0],t=this._tables[1],n=e[4],r=t[4],s=[],i=[];let a,o,c,l;for(let e=0;256>e;e++)i[(s[e]=e<<1^283*(e>>7))^e]=e;for(let u=a=0;!n[u];u^=o||1,a=i[a]||1){let i=a^a<<1^a<<2^a<<3^a<<4;i=i>>8^255&i^99,n[u]=i,r[i]=u,l=s[c=s[o=s[u]]];let f=16843009*l^65537*c^257*o^16843008*u,d=257*s[i]^16843008*i;for(let n=0;4>n;n++)e[n][u]=d=d<<24^d>>>8,t[n][i]=f=f<<24^f>>>8}for(let n=0;5>n;n++)e[n]=e[n].slice(0),t[n]=t[n].slice(0)}_crypt(e,t){if(4!==e.length)throw new d("invalid aes block size");const n=this._key[t],r=n.length/4-2,s=[0,0,0,0],i=this._tables[t],a=i[0],o=i[1],c=i[2],l=i[3],u=i[4];let f,w,h,p=e[0]^n[0],g=e[t?3:1]^n[1],m=e[2]^n[2],y=e[t?1:3]^n[3],b=4;for(let e=0;r>e;e++)f=a[p>>>24]^o[g>>16&255]^c[m>>8&255]^l[255&y]^n[b],w=a[g>>>24]^o[m>>16&255]^c[y>>8&255]^l[255&p]^n[b+1],h=a[m>>>24]^o[y>>16&255]^c[p>>8&255]^l[255&g]^n[b+2],y=a[y>>>24]^o[p>>16&255]^c[g>>8&255]^l[255&m]^n[b+3],b+=4,p=f,g=w,m=h;for(let e=0;4>e;e++)s[t?3&-e:e]=u[p>>>24]<<24^u[g>>16&255]<<16^u[m>>8&255]<<8^u[255&y]^n[b++],f=p,p=g,g=m,m=y,y=f;return s}},Fe=class{constructor(e,t){this._prf=e,this._initIv=t,this._iv=t}reset(){this._iv=this._initIv}update(e){return this.calculate(this._prf,e,this._iv)}incWord(e){if(255==(e>>24&255)){let t=e>>16&255,n=e>>8&255,r=255&e;255===t?(t=0,255===n?(n=0,255===r?r=0:++r):++n):++t,e=0,e+=t<<16,e+=n<<8,e+=r}else e+=1<<24;return e}incCounter(e){0===(e[0]=this.incWord(e[0]))&&(e[1]=this.incWord(e[1]))}calculate(e,t,n){let r;if(!(r=t.length))return[];const s=ie.bitLength(t);for(let s=0;r>s;s+=4){this.incCounter(n);const r=e.encrypt(n);t[s]^=r[0],t[s+1]^=r[1],t[s+2]^=r[2],t[s+3]^=r[3]}return ie.clamp(t,s)}},Ee=ce.hmacSha1;let Ie=ve&&_e&&typeof Ce.importKey==xe,Ae=ve&&_e&&typeof Ce.deriveBits==xe;class Te extends v{constructor({password:e,signed:t,encryptionStrength:r}){super({start(){n.assign(this,{ready:new y((e=>this.resolveReady=e)),password:e,signed:t,strength:r-1,pending:new w})},async transform(e,t){const n=this,{password:r,strength:s,resolveReady:i,ready:a}=n;r?(await(async(e,t,n,r)=>{const s=await Ue(e,t,n,We(r,0,ye[t])),i=We(r,ye[t]);if(s[0]!=i[0]||s[1]!=i[1])throw new d(ue)})(n,s,r,We(e,0,ye[s]+2)),e=We(e,ye[s]+2),i()):await a;const o=new w(e.length-Se-(e.length-Se)%we);t.enqueue(Oe(n,e,o,0,Se,!0))},async flush(e){const{signed:t,ctr:n,hmac:r,pending:s,ready:i}=this;await i;const a=We(s,0,s.length-Se),o=We(s,s.length-Se);let c=new w;if(a.length){const e=Le(De,a);r.update(e);const t=n.update(e);c=qe(De,t)}if(t){const e=We(qe(De,r.digest()),0,Se);for(let t=0;Se>t;t++)if(e[t]!=o[t])throw new d(fe)}e.enqueue(c)}})}}class He extends v{constructor({password:e,encryptionStrength:t}){let r;super({start(){n.assign(this,{ready:new y((e=>this.resolveReady=e)),password:e,strength:t-1,pending:new w})},async transform(e,t){const n=this,{password:r,strength:s,resolveReady:i,ready:a}=n;let o=new w;r?(o=await(async(e,t,n)=>{const r=de(new w(ye[t]));return Ne(r,await Ue(e,t,n,r))})(n,s,r),i()):await a;const c=new w(o.length+e.length-e.length%we);c.set(o,0),t.enqueue(Oe(n,e,c,o.length,0))},async flush(e){const{ctr:t,hmac:n,pending:s,ready:i}=this;await i;let a=new w;if(s.length){const e=t.update(Le(De,s));n.update(e),a=qe(De,e)}r.signature=qe(De,n.digest()).slice(0,Se),e.enqueue(Ne(a,r.signature))}}),r=this}}function Oe(e,t,n,r,s,i){const{ctr:a,hmac:o,pending:c}=e,l=t.length-s;let u;for(c.length&&(t=Ne(c,t),n=((e,t)=>{if(t&&t>e.length){const n=e;(e=new w(t)).set(n,0)}return e})(n,l-l%we)),u=0;l-we>=u;u+=we){const e=Le(De,We(t,u,u+we));i&&o.update(e);const s=a.update(e);i||o.update(s),n.set(qe(De,s),u+r)}return e.pending=We(t,u),n}async function Ue(e,r,s,i){e.password=null;const a=se(s),o=await(async(e,t,n,r,s)=>{if(!Ie)return ce.importKey(t);try{return await Ce.importKey("raw",t,n,!1,s)}catch(e){return Ie=!1,ce.importKey(t)}})(0,a,pe,0,me),c=await(async(e,t,n)=>{if(!Ae)return ce.pbkdf2(t,e.salt,ge.iterations,n);try{return await Ce.deriveBits(e,t,n)}catch(r){return Ae=!1,ce.pbkdf2(t,e.salt,ge.iterations,n)}})(n.assign({salt:i},ge),o,8*(2*be[r]+2)),l=new w(c),u=Le(De,We(l,0,be[r])),f=Le(De,We(l,be[r],2*be[r])),d=We(l,2*be[r]);return n.assign(e,{keys:{key:u,authentication:f,passwordVerification:d},ctr:new Fe(new Re(u),t.from(ke)),hmac:new Ee(f)}),d}function Ne(e,t){let n=e;return e.length+t.length&&(n=new w(e.length+t.length),n.set(e,0),n.set(t,e.length)),n}function We(e,t,n){return e.subarray(t,n)}function qe(e,t){return e.fromBits(t)}function Le(e,t){return e.toBits(t)}class Me extends v{constructor({password:e,passwordVerification:t}){super({start(){n.assign(this,{password:e,passwordVerification:t}),Ke(this,e)},transform(e,t){const n=this;if(n.password){const t=Ve(n,e.subarray(0,12));if(n.password=null,t[11]!=n.passwordVerification)throw new d(ue);e=e.subarray(12)}t.enqueue(Ve(n,e))}})}}class Pe extends v{constructor({password:e,passwordVerification:t}){super({start(){n.assign(this,{password:e,passwordVerification:t}),Ke(this,e)},transform(e,t){const n=this;let r,s;if(n.password){n.password=null;const t=de(new w(12));t[11]=n.passwordVerification,r=new w(e.length+t.length),r.set(Be(n,t),0),s=12}else r=new w(e.length),s=0;r.set(Be(n,e),s),t.enqueue(r)}})}}function Ve(e,t){const n=new w(t.length);for(let r=0;r>>24]),s=~e.crcKey2.get(),e.keys=[n,r,s]}function Ze(e){const t=2|e.keys[2];return Xe(a.imul(t,1^t)>>>8)}function Xe(e){return 255&e}function Ge(e){return 4294967295&e}const je="deflate-raw";class Je extends v{constructor(e,{chunkSize:t,CompressionStream:n,CompressionStreamNative:r}){super({});const{compressed:s,encrypted:i,useCompressionStream:a,zipCrypto:o,signed:c,level:l}=e,u=this;let f,d,w=$e(super.readable);i&&!o||!c||([w,f]=w.tee(),f=nt(f,new re)),s&&(w=tt(w,a,{level:l,chunkSize:t},r,n)),i&&(o?w=nt(w,new Pe(e)):(d=new He(e),w=nt(w,d))),et(u,w,(async()=>{let e;i&&!o&&(e=d.signature),i&&!o||!c||(e=await f.getReader().read(),e=new g(e.value.buffer).getUint32(0)),u.signature=e}))}}class Qe extends v{constructor(e,{chunkSize:t,DecompressionStream:n,DecompressionStreamNative:r}){super({});const{zipCrypto:s,encrypted:i,signed:a,signature:o,compressed:c,useCompressionStream:l}=e;let u,f,w=$e(super.readable);i&&(s?w=nt(w,new Me(e)):(f=new Te(e),w=nt(w,f))),c&&(w=tt(w,l,{chunkSize:t},r,n)),i&&!s||!a||([w,u]=w.tee(),u=nt(u,new re)),et(this,w,(async()=>{if((!i||s)&&a){const e=await u.getReader().read(),t=new g(e.value.buffer);if(o!=t.getUint32(0,!1))throw new d(fe)}}))}}function $e(e){return nt(e,new v({transform(e,t){e&&e.length&&t.enqueue(e)}}))}function et(e,t,r){t=nt(t,new v({flush:r})),n.defineProperty(e,"readable",{get:()=>t})}function tt(e,t,n,r,s){try{e=nt(e,new(t&&r?r:s)(je,n))}catch(r){if(!t)throw r;e=nt(e,new s(je,n))}return e}function nt(e,t){return e.pipeThrough(t)}const rt="data",st="deflate",it="inflate";class at extends v{constructor(e,t){super({});const r=this,{codecType:s}=e;let i;s.startsWith(st)?i=Je:s.startsWith(it)&&(i=Qe);let a=0;const o=new i(e,t),c=super.readable,l=new v({transform(e,t){e&&e.length&&(a+=e.length,t.enqueue(e))},flush(){const{signature:e}=o;n.assign(r,{signature:e,size:a})}});n.defineProperty(r,"readable",{get:()=>c.pipeThrough(o).pipeThrough(l)})}}const ot=typeof E!=B;class ct{constructor(e,{readable:t,writable:r},{options:s,config:i,streamOptions:a,useWebWorkers:o,transferStreams:c,scripts:l},u){const{signal:f}=a;return n.assign(e,{busy:!0,readable:t.pipeThrough(new lt(t,a,i),{signal:f}),writable:r,options:n.assign({},s),scripts:l,transferStreams:c,terminate(){const{worker:t,busy:n}=e;t&&!n&&(t.terminate(),e.interface=null)},onTaskFinished(){e.busy=!1,u(e)}}),(o&&ot?dt:ft)(e,i)}}class lt extends v{constructor(e,{onstart:t,onprogress:n,size:r,onend:s},{chunkSize:i}){let a=0;super({start(){t&&ut(t,r)},async transform(e,t){a+=e.length,n&&await ut(n,a,r),t.enqueue(e)},flush(){e.size=a,s&&ut(s,a)}},{highWaterMark:1,size:()=>i})}}async function ut(e,...t){try{await e(...t)}catch(e){}}function ft(e,t){return{run:()=>(async({options:e,readable:t,writable:n,onTaskFinished:r},s)=>{const i=new at(e,s);try{await t.pipeThrough(i).pipeTo(n,{preventClose:!0,preventAbort:!0});const{signature:e,size:s}=i;return{signature:e,size:s}}finally{r()}})(e,t)}}function dt(e,{baseURL:t,chunkSize:r}){return e.interface||n.assign(e,{worker:pt(e.scripts[0],t,e),interface:{run:()=>(async(e,t)=>{let r,s;const i=new y(((e,t)=>{r=e,s=t}));n.assign(e,{reader:null,writer:null,resolveResult:r,rejectResult:s,result:i});const{readable:a,options:o,scripts:c}=e,{writable:l,closed:u}=(e=>{const t=e.getWriter();let n;const r=new y((e=>n=e));return{writable:new _({async write(e){await t.ready,await t.write(e)},close(){t.releaseLock(),n()},abort:e=>t.abort(e)}),closed:r}})(e.writable);gt({type:"start",scripts:c.slice(1),options:o,config:t,readable:a,writable:l},e)||n.assign(e,{reader:a.getReader(),writer:l.getWriter()});const f=await i;try{await l.close()}catch(e){}return await u,f})(e,{chunkSize:r})}}),e.interface}let wt=!0,ht=!0;function pt(e,t,r){const s={type:"module"};let i,a;typeof e==K&&(e=e());try{i=new f(e,t)}catch(t){i=e}if(wt)try{a=new E(i)}catch(e){wt=!1,a=new E(i,s)}else a=new E(i,s);return a.addEventListener("message",(e=>(async({data:e},t)=>{const{type:r,value:s,messageId:i,result:a,error:o}=e,{reader:c,writer:l,resolveResult:u,rejectResult:f,onTaskFinished:h}=t;try{if(o){const{message:e,stack:t,code:r,name:s}=o,i=new d(e);n.assign(i,{stack:t,code:r,name:s}),p(i)}else{if("pull"==r){const{value:e,done:n}=await c.read();gt({type:rt,value:e,done:n,messageId:i},t)}r==rt&&(await l.ready,await l.write(new w(s)),gt({type:"ack",messageId:i},t)),"close"==r&&p(null,a)}}catch(o){p(o)}function p(e,t){e?f(e):u(t),l&&l.releaseLock(),h()}})(e,r))),a}function gt(e,{worker:t,writer:n,onTaskFinished:r,transferStreams:s}){try{let{value:n,readable:r,writable:i}=e;const a=[];if(n){const{buffer:t,length:r}=n;r!=t.byteLength&&(n=new w(n)),e.value=n.buffer,a.push(e.value)}if(s&&ht?(r&&a.push(r),i&&a.push(i)):e.readable=e.writable=null,a.length)try{return t.postMessage(e,a),!0}catch(n){ht=!1,e.readable=e.writable=null,t.postMessage(e)}else t.postMessage(e)}catch(e){throw n&&n.releaseLock(),r(),e}}let mt=[];const yt=[];let bt=0;async function St(e,t){const{options:n,config:r}=t,{transferStreams:i,useWebWorkers:a,useCompressionStream:o,codecType:c,compressed:l,signed:u,encrypted:f}=n,{workerScripts:d,maxWorkers:w,terminateWorkerTimeout:h}=r;t.transferStreams=i||i===V;const p=!(l||u||f||t.transferStreams);let g;t.useWebWorkers=!p&&(a||a===V&&r.useWebWorkers),t.scripts=t.useWebWorkers&&d?d[c]:[],n.useCompressionStream=o||o===V&&r.useCompressionStream;const m=mt.find((e=>!e.busy));if(m)kt(m),g=new ct(m,e,t,b);else if(mt.lengthyt.push({resolve:n,stream:e,workerOptions:t})));return g.run();function b(e){if(yt.length){const[{resolve:t,stream:n,workerOptions:r}]=yt.splice(0,1);t(new ct(e,n,r,b))}else e.worker?(kt(e),s.isFinite(h)&&h>=0&&(e.terminateTimeout=setTimeout((()=>{mt=mt.filter((t=>t!=e)),e.terminate()}),h))):mt=mt.filter((t=>t!=e))}}function kt(e){const{terminateTimeout:t}=e;t&&(clearTimeout(t),e.terminateTimeout=null)}const zt="HTTP error ",xt="HTTP Range not supported",vt="Writer iterator completed too soon",Ct="GET",_t=65536,Dt="writable";class Rt{constructor(){this.size=0}init(){this.initialized=!0}}class Ft extends Rt{get readable(){const e=this,{chunkSize:t=_t}=e,n=new C({start(){this.chunkOffset=0},async pull(r){const{offset:s=0,size:i,diskNumberStart:o}=n,{chunkOffset:c}=this;r.enqueue(await Qt(e,s+c,a.min(t,i-c),o)),c+t>i?r.close():this.chunkOffset+=t}});return n}}class Et extends Rt{constructor(){super();const e=this,t=new _({write:t=>e.writeUint8Array(t)});n.defineProperty(e,Dt,{get:()=>t})}writeUint8Array(){}}class It extends Ft{constructor(e){super(),n.assign(this,{blob:e,size:e.size})}async readUint8Array(e,t){const n=this,r=e+t,s=e||rt.writable}),this.blob=new u(t.readable,{headers:r}).blob()}getData(){return this.blob}}class Tt extends Ft{constructor(e,t){super(),Ot(this,e,t)}async init(){super.init(),await Ut(this,Bt,Lt)}readUint8Array(e,t){return Nt(this,e,t,Bt,Lt)}}class Ht extends Ft{constructor(e,t){super(),Ot(this,e,t)}async init(){super.init(),await Ut(this,Kt,Mt)}readUint8Array(e,t){return Nt(this,e,t,Kt,Mt)}}function Ot(e,t,r){const{preventHeadRequest:s,useRangeHeader:i,forceRangeRequests:a}=r;delete(r=n.assign({},r)).preventHeadRequest,delete r.useRangeHeader,delete r.forceRangeRequests,delete r.useXHR,n.assign(e,{url:t,options:r,preventHeadRequest:s,useRangeHeader:i,forceRangeRequests:a})}async function Ut(e,t,n){const{url:r,useRangeHeader:i,forceRangeRequests:a}=e;if((e=>{const{baseURL:t}=j(),{protocol:n}=new f(e,t);return"http:"==n||"https:"==n})(r)&&(i||a)){const{headers:r}=await t(Ct,e,Wt(e));if(!a&&"bytes"!=r.get("Accept-Ranges"))throw new d(xt);{let i;const a=r.get("Content-Range");if(a){const e=a.trim().split(/\s*\/\s*/);if(e.length){const t=e[1];t&&"*"!=t&&(i=s(t))}}i===V?await Vt(e,t,n):e.size=i}}else await Vt(e,t,n)}async function Nt(e,t,n,r,s){const{useRangeHeader:i,forceRangeRequests:a,options:o}=e;if(i||a){const s=await r(Ct,e,Wt(e,t,n));if(206!=s.status)throw new d(xt);return new w(await s.arrayBuffer())}{const{data:r}=e;return r||await s(e,o),new w(e.data.subarray(t,t+n))}}function Wt(e,t=0,r=1){return n.assign({},qt(e),{Range:"bytes="+t+"-"+(t+r-1)})}function qt({options:e}){const{headers:t}=e;if(t)return Symbol.iterator in t?n.fromEntries(t):t}async function Lt(e){await Pt(e,Bt)}async function Mt(e){await Pt(e,Kt)}async function Pt(e,t){const n=await t(Ct,e,qt(e));e.data=new w(await n.arrayBuffer()),e.size||(e.size=e.data.length)}async function Vt(e,t,n){if(e.preventHeadRequest)await n(e,e.options);else{const r=(await t("HEAD",e,qt(e))).headers.get("Content-Length");r?e.size=s(r):await n(e,e.options)}}async function Bt(e,{options:t,url:r},s){const i=await fetch(r,n.assign({},t,{method:e,headers:s}));if(400>i.status)return i;throw 416==i.status?new d(xt):new d(zt+(i.statusText||i.status))}function Kt(e,{url:t},r){return new y(((s,i)=>{const a=new XMLHttpRequest;if(a.addEventListener("load",(()=>{if(400>a.status){const e=[];a.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach((t=>{const n=t.trim().split(/\s*:\s*/);n[0]=n[0].trim().replace(/^[a-z]|-[a-z]/g,(e=>e.toUpperCase())),e.push(n)})),s({status:a.status,arrayBuffer:()=>a.response,headers:new c(e)})}else i(416==a.status?new d(xt):new d(zt+(a.statusText||a.status)))}),!1),a.addEventListener("error",(e=>i(e.detail.error)),!1),a.open(e,t),r)for(const e of n.entries(r))a.setRequestHeader(e[0],e[1]);a.responseType="arraybuffer",a.send()}))}class Yt extends Ft{constructor(e,t={}){super(),n.assign(this,{url:e,reader:t.useXHR?new Ht(e,t):new Tt(e,t)})}set size(e){}get size(){return this.reader.size}async init(){super.init(),await this.reader.init()}readUint8Array(e,t){return this.reader.readUint8Array(e,t)}}class Zt extends Ft{constructor(e){super(),this.readers=e}async init(){super.init();const e=this,{readers:t}=e;e.lastDiskNumber=0,await y.all(t.map((async t=>{await t.init(),e.size+=t.size})))}async readUint8Array(e,t,n=0){const r=this,{readers:s}=this;let i,o=n;-1==o&&(o=s.length-1);let c=e;for(;c>=s[o].size;)c-=s[o].size,o++;const l=s[o],u=l.size;if(c+t>u){const s=u-c;i=new w(t),i.set(await Qt(l,c,s)),i.set(await r.readUint8Array(e+s,t-s,n),s)}else i=await Qt(l,c,t);return r.lastDiskNumber=a.max(o,r.lastDiskNumber),i}}class Xt extends Rt{constructor(e,t=4294967295){super();const r=this;let s,i,a;n.assign(r,{diskNumber:0,diskOffset:0,size:0,maxSize:t,availableSize:t});const o=new _({async write(t){const{availableSize:n}=r;if(a)t.lengtho})}}async function Gt(e,t){e.init&&!e.initialized&&await e.init(t)}function jt(e){return t.isArray(e)&&(e=new Zt(e)),e instanceof C&&(e={readable:e}),e}function Jt(e){e.writable===V&&typeof e.next==K&&(e=new Xt(e)),e instanceof _&&(e={writable:e});const{writable:t}=e;return t.size===V&&(t.size=0),e instanceof Xt||n.assign(e,{diskNumber:0,diskOffset:0,availableSize:1/0,maxSize:1/0}),e}function Qt(e,t,n,r){return e.readUint8Array(t,n,r)}const $t=Zt,en=Xt,tn="\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ".split("");function nn(e,t){return t&&"cp437"==t.trim().toLowerCase()?(e=>{{let t="";for(let n=0;nthis[t]=e[t]))}}const zn="File format is not recognized",xn="End of central directory not found",vn="End of Zip64 central directory not found",Cn="End of Zip64 central directory locator not found",_n="Central directory header not found",Dn="Local file header not found",Rn="Zip64 extra field not found",Fn="File contains encrypted entry",En="Encryption method not supported",In="Compression method not supported",An="Split zip file",Tn="utf-8",Hn="cp437",On=[[cn,I],[ln,I],[un,I],[fn,A]],Un={[A]:{getValue:Yn,bytes:4},[I]:{getValue:Zn,bytes:8}};class Nn{constructor(e,t,r){n.assign(this,{reader:e,config:t,options:r})}async getData(e,t,r={}){const s=this,{reader:i,offset:a,diskNumberStart:o,extraFieldAES:c,compressionMethod:l,config:u,bitFlag:f,signature:h,rawLastModDate:p,uncompressedSize:g,compressedSize:m}=s,y=s.localDirectory={},b=Xn(await Qt(i,a,30,o));let S=Mn(s,r,"password");if(S=S&&S.length&&S,c&&99!=c.originalCompressionMethod)throw new d(In);if(0!=l&&8!=l)throw new d(In);if(Yn(b,0)!=T)throw new d(Dn);Wn(y,b,4),y.rawExtraField=y.extraFieldLength?await Qt(i,a+30+y.filenameLength,y.extraFieldLength,o):new w,await qn(s,y,b,4),n.assign(t,{lastAccessDate:y.lastAccessDate,creationDate:y.creationDate});const k=s.encrypted&&y.encrypted,z=k&&!c;if(k){if(!z&&c.strength===V)throw new d(En);if(!S)throw new d(Fn)}const x=a+30+y.filenameLength+y.extraFieldLength,v=i.readable;v.diskNumberStart=o,v.offset=x;const C=v.size=m,_=Mn(s,r,"signal");e=Jt(e),await Gt(e,g);const{writable:D}=e,{onstart:R,onprogress:F,onend:E}=r,I={options:{codecType:it,password:S,zipCrypto:z,encryptionStrength:c&&c.strength,signed:Mn(s,r,"checkSignature"),passwordVerification:z&&(f.dataDescriptor?p>>>8&255:h>>>24&255),signature:h,compressed:0!=l,encrypted:k,useWebWorkers:Mn(s,r,"useWebWorkers"),useCompressionStream:Mn(s,r,"useCompressionStream"),transferStreams:Mn(s,r,"transferStreams")},config:u,streamOptions:{signal:_,size:C,onstart:R,onprogress:F,onend:E}};return D.size+=(await St({readable:v,writable:D},I)).size,Mn(s,r,"preventClose")||await D.close(),e.getData?e.getData():D}}function Wn(e,t,r){const s=e.rawBitFlag=Kn(t,r+2),i=1==(1&s),a=Yn(t,r+6);n.assign(e,{encrypted:i,version:Kn(t,r),bitFlag:{level:(6&s)>>1,dataDescriptor:8==(8&s),languageEncodingFlag:(s&q)==q},rawLastModDate:a,lastModDate:Pn(a),filenameLength:Kn(t,r+22),extraFieldLength:Kn(t,r+24)})}async function qn(e,t,r,s){const{rawExtraField:i}=t,a=t.extraField=new c,l=Xn(new w(i));let u=0;try{for(;u{t.zip64=!0;const n=Xn(e.data),r=On.filter((([e,n])=>t[e]==n));for(let s=0,i=0;s{const s=Xn(e.data),i=Bn(s,4);n.assign(e,{vendorVersion:Bn(s,0),vendorId:Bn(s,2),strength:i,originalCompressionMethod:r,compressionMethod:Kn(s,5)}),t.compressionMethod=e.compressionMethod})(m,t,f),t.extraFieldAES=m):t.compressionMethod=f;const y=a.get(10);y&&(((e,t)=>{const r=Xn(e.data);let s,i=4;try{for(;i{const n=Xn(e.data),r=Bn(n,0),s=[],i=[];1==(1&r)&&(s.push(dn),i.push(wn)),2==(2&r)&&(s.push(hn),i.push("rawLastAccessDate")),4==(4&r)&&(s.push(pn),i.push("rawCreationDate"));let a=1;s.forEach(((r,s)=>{if(e.data.length>=a+4){const c=Yn(n,a);t[r]=e[r]=new o(1e3*c);const l=i[s];e[l]=c}a+=4}))})(b,t),t.extraFieldExtendedTimestamp=b)}async function Ln(e,t,r,s,i){const a=Xn(e.data),o=new ne;o.append(i[r]);const c=Xn(new w(4));c.setUint32(0,o.get(),!0),n.assign(e,{version:Bn(a,0),signature:Yn(a,1),[t]:await nn(e.data.subarray(5)),valid:!i.bitFlag.languageEncodingFlag&&e.signature==Yn(c,0)}),e.valid&&(s[t]=e[t],s[t+"UTF8"]=!0)}function Mn(e,t,n){return t[n]===V?e.options[n]:t[n]}function Pn(e){const t=(4294901760&e)>>16,n=65535&e;try{return new o(1980+((65024&t)>>9),((480&t)>>5)-1,31&t,(63488&n)>>11,(2016&n)>>5,2*(31&n),0)}catch(e){}}function Vn(e){return new o(s(e/i(1e4)-i(116444736e5)))}function Bn(e,t){return e.getUint8(t)}function Kn(e,t){return e.getUint16(t,!0)}function Yn(e,t){return e.getUint32(t,!0)}function Zn(e,t){return s(e.getBigUint64(t,!0))}function Xn(e){return new g(e.buffer)}const Gn="File already exists",jn="Zip file comment exceeds 64KB",Jn="File entry comment exceeds 64KB",Qn="File entry name exceeds 64KB",$n="Version exceeds 65535",er="The strength must equal 1, 2, or 3",tr="Extra field type exceeds 65535",nr="Extra field data exceeds 64KB",rr="Zip64 is not supported (make sure 'keepOrder' is set to 'true')",sr=new w([7,0,2,0,65,69,3,0,0]);let ir=0;const ar=[];async function or(e,t){const n=e.getWriter();await n.ready,e.size+=gr(t),await n.write(t),n.releaseLock()}function cr(e){if(e)return(i(e.getTime())+i(116444736e5))*i(1e4)}function lr(e,t,n,r){const s=t[n]===V?e.options[n]:t[n];return s===V?r:s}function ur(e,t,n){e.setUint8(t,n)}function fr(e,t,n){e.setUint16(t,n,!0)}function dr(e,t,n){e.setUint32(t,n,!0)}function wr(e,t,n){e.setBigUint64(t,n,!0)}function hr(e,t,n){e.set(t,n)}function pr(e){return new g(e.buffer)}function gr(...e){let t=0;return e.forEach((e=>e&&(t+=e.length))),t}let mr;try{mr=void 0===k&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:void 0===k?location.href:k.currentScript&&k.currentScript.src||new f("zip.min.js",k.baseURI).href}catch(e){}Q({baseURL:mr}),(e=>{const t=()=>f.createObjectURL(new m(['const{Array:e,Object:t,Number:n,Math:r,Error:s,Uint8Array:i,Uint16Array:o,Uint32Array:c,Int32Array:f,Map:a,DataView:l,Promise:u,TextEncoder:w,crypto:h,postMessage:d,TransformStream:p,ReadableStream:y,WritableStream:m,CompressionStream:b,DecompressionStream:g}=self;class k{constructor(e){return class extends p{constructor(t,n){const r=new e(n);super({transform(e,t){t.enqueue(r.append(e))},flush(e){const t=r.flush();t&&e.enqueue(t)}})}}}}const v=[];for(let e=0;256>e;e++){let t=e;for(let e=0;8>e;e++)1&t?t=t>>>1^3988292384:t>>>=1;v[e]=t}class S{constructor(e){this.t=e||-1}append(e){let t=0|this.t;for(let n=0,r=0|e.length;r>n;n++)t=t>>>8^v[255&(t^e[n])];this.t=t}get(){return~this.t}}class z extends p{constructor(){const e=new S;super({transform(t){e.append(t)},flush(t){const n=new i(4);new l(n.buffer).setUint32(0,e.get()),t.enqueue(n)}})}}const C={concat(e,t){if(0===e.length||0===t.length)return e.concat(t);const n=e[e.length-1],r=C.i(n);return 32===r?e.concat(t):C.o(t,r,0|n,e.slice(0,e.length-1))},l(e){const t=e.length;if(0===t)return 0;const n=e[t-1];return 32*(t-1)+C.i(n)},u(e,t){if(32*e.length0&&t&&(e[n-1]=C.h(t,e[n-1]&2147483648>>t-1,1)),e},h:(e,t,n)=>32===e?t:(n?0|t:t<<32-e)+1099511627776*e,i:e=>r.round(e/1099511627776)||32,o(e,t,n,r){for(void 0===r&&(r=[]);t>=32;t-=32)r.push(n),n=0;if(0===t)return r.concat(e);for(let s=0;s>>t),n=e[s]<<32-t;const s=e.length?e[e.length-1]:0,i=C.i(s);return r.push(C.h(t+i&31,t+i>32?n:r.pop(),1)),r}},I={p:{m(e){const t=C.l(e)/8,n=new i(t);let r;for(let s=0;t>s;s++)0==(3&s)&&(r=e[s/4]),n[s]=r>>>24,r<<=8;return n},g(e){const t=[];let n,r=0;for(n=0;n{let t=987654321;const n=4294967295;return()=>(t=36969*(65535&t)+(t>>16)&n,(((t<<16)+(e=18e3*(65535&e)+(e>>16)&n)&n)/4294967296+.5)*(r.random()>.5?1:-1))};for(let s,i=0;inew _.k(I.p.g(e)),v(e,t,n,r){if(n=n||1e4,0>r||0>n)throw new s("invalid params to pbkdf2");const i=1+(r>>5)<<2;let o,c,f,a,u;const w=new ArrayBuffer(i),h=new l(w);let d=0;const p=C;for(t=I.p.g(t),u=1;(i||1)>d;u++){for(o=c=e.encrypt(p.concat(t,[u])),f=1;n>f;f++)for(c=e.encrypt(c),a=0;ad&&f9007199254740991)throw new s("Cannot hash more than 2^53 - 1 bits");const o=new c(n);let f=0;for(let e=t.blockSize+r-(t.blockSize+r&t.blockSize-1);i>=e;e+=t.blockSize)t.R(o.subarray(16*f,16*(f+1))),f+=1;return n.splice(0,16*f),t}B(){const e=this;let t=e.A;const n=e._;t=C.concat(t,[C.h(1,1)]);for(let e=t.length+2;15&e;e++)t.push(0);for(t.push(r.floor(e.D/4294967296)),t.push(0|e.D);t.length;)e.R(t.splice(0,16));return e.reset(),n}M(e,t,n,r){return e>19?e>39?e>59?e>79?void 0:t^n^r:t&n|t&r|n&r:t^n^r:t&n|~t&r}K(e,t){return t<>>32-e}R(t){const n=this,s=n._,i=e(80);for(let e=0;16>e;e++)i[e]=t[e];let o=s[0],c=s[1],f=s[2],a=s[3],l=s[4];for(let e=0;79>=e;e++){16>e||(i[e]=n.K(1,i[e-3]^i[e-8]^i[e-14]^i[e-16]));const t=n.K(5,o)+n.M(e,c,f,a)+l+i[e]+n.I[r.floor(e/20)]|0;l=a,a=f,f=n.K(30,c),c=o,o=t}s[0]=s[0]+o|0,s[1]=s[1]+c|0,s[2]=s[2]+f|0,s[3]=s[3]+a|0,s[4]=s[4]+l|0}},o=[[],[]];n.P=[new i,new i];const f=n.P[0].blockSize/32;t.length>f&&(t=i.hash(t));for(let e=0;f>e;e++)o[0][e]=909522486^t[e],o[1][e]=1549556828^t[e];n.P[0].update(o[0]),n.P[1].update(o[1]),n.U=new i(n.P[0])}reset(){const e=this;e.U=new e.S(e.P[0]),e.N=!1}update(e){this.N=!0,this.U.update(e)}digest(){const e=this,t=e.U.B(),n=new e.S(e.P[1]).update(t).B();return e.reset(),n}encrypt(e){if(this.N)throw new s("encrypt on already updated hmac called!");return this.update(e),this.digest(e)}}},A=void 0!==h&&"function"==typeof h.getRandomValues;function D(e){return A?h.getRandomValues(e):x.getRandomValues(e)}const V={name:"PBKDF2"},R=t.assign({hash:{name:"HMAC"}},V),B=t.assign({iterations:1e3,hash:{name:"SHA-1"}},V),E=["deriveBits"],M=[8,12,16],K=[16,24,32],P=[0,0,0,0],U=void 0!==h,N=U&&h.subtle,T=U&&void 0!==N,W=I.p,H=class{constructor(e){const t=this;t.T=[[[],[],[],[],[]],[[],[],[],[],[]]],t.T[0][0][0]||t.W();const n=t.T[0][4],r=t.T[1],i=e.length;let o,c,f,a=1;if(4!==i&&6!==i&&8!==i)throw new s("invalid aes key size");for(t.I=[c=e.slice(0),f=[]],o=i;4*i+28>o;o++){let e=c[o-1];(o%i==0||8===i&&o%i==4)&&(e=n[e>>>24]<<24^n[e>>16&255]<<16^n[e>>8&255]<<8^n[255&e],o%i==0&&(e=e<<8^e>>>24^a<<24,a=a<<1^283*(a>>7))),c[o]=c[o-i]^e}for(let e=0;o;e++,o--){const t=c[3&e?o:o-4];f[e]=4>=o||4>e?t:r[0][n[t>>>24]]^r[1][n[t>>16&255]]^r[2][n[t>>8&255]]^r[3][n[255&t]]}}encrypt(e){return this.H(e,0)}decrypt(e){return this.H(e,1)}W(){const e=this.T[0],t=this.T[1],n=e[4],r=t[4],s=[],i=[];let o,c,f,a;for(let e=0;256>e;e++)i[(s[e]=e<<1^283*(e>>7))^e]=e;for(let l=o=0;!n[l];l^=c||1,o=i[o]||1){let i=o^o<<1^o<<2^o<<3^o<<4;i=i>>8^255&i^99,n[l]=i,r[i]=l,a=s[f=s[c=s[l]]];let u=16843009*a^65537*f^257*c^16843008*l,w=257*s[i]^16843008*i;for(let n=0;4>n;n++)e[n][l]=w=w<<24^w>>>8,t[n][i]=u=u<<24^u>>>8}for(let n=0;5>n;n++)e[n]=e[n].slice(0),t[n]=t[n].slice(0)}H(e,t){if(4!==e.length)throw new s("invalid aes block size");const n=this.I[t],r=n.length/4-2,i=[0,0,0,0],o=this.T[t],c=o[0],f=o[1],a=o[2],l=o[3],u=o[4];let w,h,d,p=e[0]^n[0],y=e[t?3:1]^n[1],m=e[2]^n[2],b=e[t?1:3]^n[3],g=4;for(let e=0;r>e;e++)w=c[p>>>24]^f[y>>16&255]^a[m>>8&255]^l[255&b]^n[g],h=c[y>>>24]^f[m>>16&255]^a[b>>8&255]^l[255&p]^n[g+1],d=c[m>>>24]^f[b>>16&255]^a[p>>8&255]^l[255&y]^n[g+2],b=c[b>>>24]^f[p>>16&255]^a[y>>8&255]^l[255&m]^n[g+3],g+=4,p=w,y=h,m=d;for(let e=0;4>e;e++)i[t?3&-e:e]=u[p>>>24]<<24^u[y>>16&255]<<16^u[m>>8&255]<<8^u[255&b]^n[g++],w=p,p=y,y=m,m=b,b=w;return i}},L=class{constructor(e,t){this.L=e,this.j=t,this.F=t}reset(){this.F=this.j}update(e){return this.O(this.L,e,this.F)}q(e){if(255==(e>>24&255)){let t=e>>16&255,n=e>>8&255,r=255&e;255===t?(t=0,255===n?(n=0,255===r?r=0:++r):++n):++t,e=0,e+=t<<16,e+=n<<8,e+=r}else e+=1<<24;return e}G(e){0===(e[0]=this.q(e[0]))&&(e[1]=this.q(e[1]))}O(e,t,n){let r;if(!(r=t.length))return[];const s=C.l(t);for(let s=0;r>s;s+=4){this.G(n);const r=e.encrypt(n);t[s]^=r[0],t[s+1]^=r[1],t[s+2]^=r[2],t[s+3]^=r[3]}return C.u(t,s)}},j=_.k;let F=U&&T&&"function"==typeof N.importKey,O=U&&T&&"function"==typeof N.deriveBits;class q extends p{constructor({password:e,signed:n,encryptionStrength:r}){super({start(){t.assign(this,{ready:new u((e=>this.J=e)),password:e,signed:n,X:r-1,pending:new i})},async transform(e,t){const n=this,{password:r,X:o,J:c,ready:f}=n;r?(await(async(e,t,n,r)=>{const i=await Q(e,t,n,Y(r,0,M[t])),o=Y(r,M[t]);if(i[0]!=o[0]||i[1]!=o[1])throw new s("Invalid password")})(n,o,r,Y(e,0,M[o]+2)),e=Y(e,M[o]+2),c()):await f;const a=new i(e.length-10-(e.length-10)%16);t.enqueue(J(n,e,a,0,10,!0))},async flush(e){const{signed:t,Y:n,Z:r,pending:o,ready:c}=this;await c;const f=Y(o,0,o.length-10),a=Y(o,o.length-10);let l=new i;if(f.length){const e=$(W,f);r.update(e);const t=n.update(e);l=Z(W,t)}if(t){const e=Y(Z(W,r.digest()),0,10);for(let t=0;10>t;t++)if(e[t]!=a[t])throw new s("Invalid signature")}e.enqueue(l)}})}}class G extends p{constructor({password:e,encryptionStrength:n}){let r;super({start(){t.assign(this,{ready:new u((e=>this.J=e)),password:e,X:n-1,pending:new i})},async transform(e,t){const n=this,{password:r,X:s,J:o,ready:c}=n;let f=new i;r?(f=await(async(e,t,n)=>{const r=D(new i(M[t]));return X(r,await Q(e,t,n,r))})(n,s,r),o()):await c;const a=new i(f.length+e.length-e.length%16);a.set(f,0),t.enqueue(J(n,e,a,f.length,0))},async flush(e){const{Y:t,Z:n,pending:s,ready:o}=this;await o;let c=new i;if(s.length){const e=t.update($(W,s));n.update(e),c=Z(W,e)}r.signature=Z(W,n.digest()).slice(0,10),e.enqueue(X(c,r.signature))}}),r=this}}function J(e,t,n,r,s,o){const{Y:c,Z:f,pending:a}=e,l=t.length-s;let u;for(a.length&&(t=X(a,t),n=((e,t)=>{if(t&&t>e.length){const n=e;(e=new i(t)).set(n,0)}return e})(n,l-l%16)),u=0;l-16>=u;u+=16){const e=$(W,Y(t,u,u+16));o&&f.update(e);const s=c.update(e);o||f.update(s),n.set(Z(W,s),u+r)}return e.pending=Y(t,u),n}async function Q(n,r,s,o){n.password=null;const c=(e=>{if(void 0===w){const t=new i((e=unescape(encodeURIComponent(e))).length);for(let n=0;n{if(!F)return _.importKey(t);try{return await N.importKey("raw",t,n,!1,s)}catch(e){return F=!1,_.importKey(t)}})(0,c,R,0,E),a=await(async(e,t,n)=>{if(!O)return _.v(t,e.salt,B.iterations,n);try{return await N.deriveBits(e,t,n)}catch(r){return O=!1,_.v(t,e.salt,B.iterations,n)}})(t.assign({salt:o},B),f,8*(2*K[r]+2)),l=new i(a),u=$(W,Y(l,0,K[r])),h=$(W,Y(l,K[r],2*K[r])),d=Y(l,2*K[r]);return t.assign(n,{keys:{key:u,$:h,passwordVerification:d},Y:new L(new H(u),e.from(P)),Z:new j(h)}),d}function X(e,t){let n=e;return e.length+t.length&&(n=new i(e.length+t.length),n.set(e,0),n.set(t,e.length)),n}function Y(e,t,n){return e.subarray(t,n)}function Z(e,t){return e.m(t)}function $(e,t){return e.g(t)}class ee extends p{constructor({password:e,passwordVerification:n}){super({start(){t.assign(this,{password:e,passwordVerification:n}),se(this,e)},transform(e,t){const n=this;if(n.password){const t=ne(n,e.subarray(0,12));if(n.password=null,t[11]!=n.passwordVerification)throw new s("Invalid password");e=e.subarray(12)}t.enqueue(ne(n,e))}})}}class te extends p{constructor({password:e,passwordVerification:n}){super({start(){t.assign(this,{password:e,passwordVerification:n}),se(this,e)},transform(e,t){const n=this;let r,s;if(n.password){n.password=null;const t=D(new i(12));t[11]=n.passwordVerification,r=new i(e.length+t.length),r.set(re(n,t),0),s=12}else r=new i(e.length),s=0;r.set(re(n,e),s),t.enqueue(r)}})}}function ne(e,t){const n=new i(t.length);for(let r=0;r>>24]),i=~e.te.get(),e.keys=[n,s,i]}function oe(e){const t=2|e.keys[2];return ce(r.imul(t,1^t)>>>8)}function ce(e){return 255&e}function fe(e){return 4294967295&e}class ae extends p{constructor(e,{chunkSize:t,CompressionStream:n,CompressionStreamNative:r}){super({});const{compressed:s,encrypted:i,useCompressionStream:o,zipCrypto:c,signed:f,level:a}=e,u=this;let w,h,d=ue(super.readable);i&&!c||!f||([d,w]=d.tee(),w=de(w,new z)),s&&(d=he(d,o,{level:a,chunkSize:t},r,n)),i&&(c?d=de(d,new te(e)):(h=new G(e),d=de(d,h))),we(u,d,(async()=>{let e;i&&!c&&(e=h.signature),i&&!c||!f||(e=await w.getReader().read(),e=new l(e.value.buffer).getUint32(0)),u.signature=e}))}}class le extends p{constructor(e,{chunkSize:t,DecompressionStream:n,DecompressionStreamNative:r}){super({});const{zipCrypto:i,encrypted:o,signed:c,signature:f,compressed:a,useCompressionStream:u}=e;let w,h,d=ue(super.readable);o&&(i?d=de(d,new ee(e)):(h=new q(e),d=de(d,h))),a&&(d=he(d,u,{chunkSize:t},r,n)),o&&!i||!c||([d,w]=d.tee(),w=de(w,new z)),we(this,d,(async()=>{if((!o||i)&&c){const e=await w.getReader().read(),t=new l(e.value.buffer);if(f!=t.getUint32(0,!1))throw new s("Invalid signature")}}))}}function ue(e){return de(e,new p({transform(e,t){e&&e.length&&t.enqueue(e)}}))}function we(e,n,r){n=de(n,new p({flush:r})),t.defineProperty(e,"readable",{get:()=>n})}function he(e,t,n,r,s){try{e=de(e,new(t&&r?r:s)("deflate-raw",n))}catch(r){if(!t)throw r;e=de(e,new s("deflate-raw",n))}return e}function de(e,t){return e.pipeThrough(t)}class pe extends p{constructor(e,n){super({});const r=this,{codecType:s}=e;let i;s.startsWith("deflate")?i=ae:s.startsWith("inflate")&&(i=le);let o=0;const c=new i(e,n),f=super.readable,a=new p({transform(e,t){e&&e.length&&(o+=e.length,t.enqueue(e))},flush(){const{signature:e}=c;t.assign(r,{signature:e,size:o})}});t.defineProperty(r,"readable",{get:()=>f.pipeThrough(c).pipeThrough(a)})}}const ye=new a,me=new a;let be=0;async function ge(e){try{const{options:t,scripts:r,config:s}=e;r&&r.length&&importScripts.apply(void 0,r),self.initCodec&&self.initCodec(),s.CompressionStreamNative=self.CompressionStream,s.DecompressionStreamNative=self.DecompressionStream,self.Deflate&&(s.CompressionStream=new k(self.Deflate)),self.Inflate&&(s.DecompressionStream=new k(self.Inflate));const i={highWaterMark:1,size:()=>s.chunkSize},o=e.readable||new y({async pull(e){const t=new u((e=>ye.set(be,e)));ke({type:"pull",messageId:be}),be=(be+1)%n.MAX_SAFE_INTEGER;const{value:r,done:s}=await t;e.enqueue(r),s&&e.close()}},i),c=e.writable||new m({async write(e){let t;const r=new u((e=>t=e));me.set(be,t),ke({type:"data",value:e,messageId:be}),be=(be+1)%n.MAX_SAFE_INTEGER,await r}},i),f=new pe(t,s);await o.pipeThrough(f).pipeTo(c,{preventAbort:!0});try{await c.close()}catch(e){}const{signature:a,size:l}=f;ke({type:"close",result:{signature:a,size:l}})}catch(e){ve(e)}}function ke(e){let{value:t}=e;if(t)if(t.length)try{t=new i(t),e.value=t.buffer,d(e,[e.value])}catch(t){d(e)}else d(e);else d(e)}function ve(e){const{message:t,stack:n,code:r,name:s}=e;d({error:{message:t,stack:n,code:r,name:s}})}function Se(t){return ze(t.map((([t,n])=>new e(t).fill(n,0,t))))}function ze(t){return t.reduce(((t,n)=>t.concat(e.isArray(n)?ze(n):n)),[])}addEventListener("message",(({data:e})=>{const{type:t,messageId:n,value:r,done:s}=e;try{if("start"==t&&ge(e),"data"==t){const e=ye.get(n);ye.delete(n),e({value:new i(r),done:s})}if("ack"==t){const e=me.get(n);me.delete(n),e()}}catch(e){ve(e)}}));const Ce=[0,1,2,3].concat(...Se([[2,4],[2,5],[4,6],[4,7],[8,8],[8,9],[16,10],[16,11],[32,12],[32,13],[64,14],[64,15],[2,0],[1,16],[1,17],[2,18],[2,19],[4,20],[4,21],[8,22],[8,23],[16,24],[16,25],[32,26],[32,27],[64,28],[64,29]]));function Ie(){const e=this;function t(e,t){let n=0;do{n|=1&e,e>>>=1,n<<=1}while(--t>0);return n>>>1}e.ne=n=>{const s=e.re,i=e.ie.se,o=e.ie.oe;let c,f,a,l=-1;for(n.ce=0,n.fe=573,c=0;o>c;c++)0!==s[2*c]?(n.ae[++n.ce]=l=c,n.le[c]=0):s[2*c+1]=0;for(;2>n.ce;)a=n.ae[++n.ce]=2>l?++l:0,s[2*a]=1,n.le[a]=0,n.ue--,i&&(n.we-=i[2*a+1]);for(e.he=l,c=r.floor(n.ce/2);c>=1;c--)n.de(s,c);a=o;do{c=n.ae[1],n.ae[1]=n.ae[n.ce--],n.de(s,1),f=n.ae[1],n.ae[--n.fe]=c,n.ae[--n.fe]=f,s[2*a]=s[2*c]+s[2*f],n.le[a]=r.max(n.le[c],n.le[f])+1,s[2*c+1]=s[2*f+1]=a,n.ae[1]=a++,n.de(s,1)}while(n.ce>=2);n.ae[--n.fe]=n.ae[1],(t=>{const n=e.re,r=e.ie.se,s=e.ie.pe,i=e.ie.ye,o=e.ie.me;let c,f,a,l,u,w,h=0;for(l=0;15>=l;l++)t.be[l]=0;for(n[2*t.ae[t.fe]+1]=0,c=t.fe+1;573>c;c++)f=t.ae[c],l=n[2*n[2*f+1]+1]+1,l>o&&(l=o,h++),n[2*f+1]=l,f>e.he||(t.be[l]++,u=0,i>f||(u=s[f-i]),w=n[2*f],t.ue+=w*(l+u),r&&(t.we+=w*(r[2*f+1]+u)));if(0!==h){do{for(l=o-1;0===t.be[l];)l--;t.be[l]--,t.be[l+1]+=2,t.be[o]--,h-=2}while(h>0);for(l=o;0!==l;l--)for(f=t.be[l];0!==f;)a=t.ae[--c],a>e.he||(n[2*a+1]!=l&&(t.ue+=(l-n[2*a+1])*n[2*a],n[2*a+1]=l),f--)}})(n),((e,n,r)=>{const s=[];let i,o,c,f=0;for(i=1;15>=i;i++)s[i]=f=f+r[i-1]<<1;for(o=0;n>=o;o++)c=e[2*o+1],0!==c&&(e[2*o]=t(s[c]++,c))})(s,e.he,n.be)}}function xe(e,t,n,r,s){const i=this;i.se=e,i.pe=t,i.ye=n,i.oe=r,i.me=s}Ie.ge=[0,1,2,3,4,5,6,7].concat(...Se([[2,8],[2,9],[2,10],[2,11],[4,12],[4,13],[4,14],[4,15],[8,16],[8,17],[8,18],[8,19],[16,20],[16,21],[16,22],[16,23],[32,24],[32,25],[32,26],[31,27],[1,28]])),Ie.ke=[0,1,2,3,4,5,6,7,8,10,12,14,16,20,24,28,32,40,48,56,64,80,96,112,128,160,192,224,0],Ie.ve=[0,1,2,3,4,6,8,12,16,24,32,48,64,96,128,192,256,384,512,768,1024,1536,2048,3072,4096,6144,8192,12288,16384,24576],Ie.Se=e=>256>e?Ce[e]:Ce[256+(e>>>7)],Ie.ze=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],Ie.Ce=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Ie.Ie=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],Ie.xe=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];const _e=Se([[144,8],[112,9],[24,7],[8,8]]);xe._e=ze([12,140,76,204,44,172,108,236,28,156,92,220,60,188,124,252,2,130,66,194,34,162,98,226,18,146,82,210,50,178,114,242,10,138,74,202,42,170,106,234,26,154,90,218,58,186,122,250,6,134,70,198,38,166,102,230,22,150,86,214,54,182,118,246,14,142,78,206,46,174,110,238,30,158,94,222,62,190,126,254,1,129,65,193,33,161,97,225,17,145,81,209,49,177,113,241,9,137,73,201,41,169,105,233,25,153,89,217,57,185,121,249,5,133,69,197,37,165,101,229,21,149,85,213,53,181,117,245,13,141,77,205,45,173,109,237,29,157,93,221,61,189,125,253,19,275,147,403,83,339,211,467,51,307,179,435,115,371,243,499,11,267,139,395,75,331,203,459,43,299,171,427,107,363,235,491,27,283,155,411,91,347,219,475,59,315,187,443,123,379,251,507,7,263,135,391,71,327,199,455,39,295,167,423,103,359,231,487,23,279,151,407,87,343,215,471,55,311,183,439,119,375,247,503,15,271,143,399,79,335,207,463,47,303,175,431,111,367,239,495,31,287,159,415,95,351,223,479,63,319,191,447,127,383,255,511,0,64,32,96,16,80,48,112,8,72,40,104,24,88,56,120,4,68,36,100,20,84,52,116,3,131,67,195,35,163,99,227].map(((e,t)=>[e,_e[t]])));const Ae=Se([[30,5]]);function De(e,t,n,r,s){const i=this;i.Ae=e,i.De=t,i.Ve=n,i.Re=r,i.Be=s}xe.Ee=ze([0,16,8,24,4,20,12,28,2,18,10,26,6,22,14,30,1,17,9,25,5,21,13,29,3,19,11,27,7,23].map(((e,t)=>[e,Ae[t]]))),xe.Me=new xe(xe._e,Ie.ze,257,286,15),xe.Ke=new xe(xe.Ee,Ie.Ce,0,30,15),xe.Pe=new xe(null,Ie.Ie,0,19,7);const Ve=[new De(0,0,0,0,0),new De(4,4,8,4,1),new De(4,5,16,8,1),new De(4,6,32,32,1),new De(4,4,16,16,2),new De(8,16,32,32,2),new De(8,16,128,128,2),new De(8,32,128,256,2),new De(32,128,258,1024,2),new De(32,258,258,4096,2)],Re=["need dictionary","stream end","","","stream error","data error","","buffer error","",""];function Be(e,t,n,r){const s=e[2*t],i=e[2*n];return i>s||s==i&&r[t]<=r[n]}function Ee(){const e=this;let t,n,s,c,f,a,l,u,w,h,d,p,y,m,b,g,k,v,S,z,C,I,x,_,A,D,V,R,B,E,M,K,P;const U=new Ie,N=new Ie,T=new Ie;let W,H,L,j,F,O;function q(){let t;for(t=0;286>t;t++)M[2*t]=0;for(t=0;30>t;t++)K[2*t]=0;for(t=0;19>t;t++)P[2*t]=0;M[512]=1,e.ue=e.we=0,H=L=0}function G(e,t){let n,r=-1,s=e[1],i=0,o=7,c=4;0===s&&(o=138,c=3),e[2*(t+1)+1]=65535;for(let f=0;t>=f;f++)n=s,s=e[2*(f+1)+1],++ii?P[2*n]+=i:0!==n?(n!=r&&P[2*n]++,P[32]++):i>10?P[36]++:P[34]++,i=0,r=n,0===s?(o=138,c=3):n==s?(o=6,c=3):(o=7,c=4))}function J(t){e.Ue[e.pending++]=t}function Q(e){J(255&e),J(e>>>8&255)}function X(e,t){let n;const r=t;O>16-r?(n=e,F|=n<>>16-O,O+=r-16):(F|=e<=n;n++)if(r=i,i=e[2*(n+1)+1],++o>=c||r!=i){if(f>o)do{Y(r,P)}while(0!=--o);else 0!==r?(r!=s&&(Y(r,P),o--),Y(16,P),X(o-3,2)):o>10?(Y(18,P),X(o-11,7)):(Y(17,P),X(o-3,3));o=0,s=r,0===i?(c=138,f=3):r==i?(c=6,f=3):(c=7,f=4)}}function $(){16==O?(Q(F),F=0,O=0):8>O||(J(255&F),F>>>=8,O-=8)}function ee(t,n){let s,i,o;if(e.Ne[H]=t,e.Te[H]=255&n,H++,0===t?M[2*n]++:(L++,t--,M[2*(Ie.ge[n]+256+1)]++,K[2*Ie.Se(t)]++),0==(8191&H)&&V>2){for(s=8*H,i=C-k,o=0;30>o;o++)s+=K[2*o]*(5+Ie.Ce[o]);if(s>>>=3,Lc);Y(256,t),j=t[513]}function ne(){O>8?Q(F):O>0&&J(255&F),F=0,O=0}function re(t,n,r){X(0+(r?1:0),3),((t,n)=>{ne(),j=8,Q(n),Q(~n),e.Ue.set(u.subarray(t,t+n),e.pending),e.pending+=n})(t,n)}function se(n){((t,n,r)=>{let s,i,o=0;V>0?(U.ne(e),N.ne(e),o=(()=>{let t;for(G(M,U.he),G(K,N.he),T.ne(e),t=18;t>=3&&0===P[2*Ie.xe[t]+1];t--);return e.ue+=14+3*(t+1),t})(),s=e.ue+3+7>>>3,i=e.we+3+7>>>3,i>s||(s=i)):s=i=n+5,n+4>s||-1==t?i==s?(X(2+(r?1:0),3),te(xe._e,xe.Ee)):(X(4+(r?1:0),3),((e,t,n)=>{let r;for(X(e-257,5),X(t-1,5),X(n-4,4),r=0;n>r;r++)X(P[2*Ie.xe[r]+1],3);Z(M,e-1),Z(K,t-1)})(U.he+1,N.he+1,o+1),te(M,K)):re(t,n,r),q(),r&&ne()})(0>k?-1:k,C-k,n),k=C,t.We()}function ie(){let e,n,r,s;do{if(s=w-x-C,0===s&&0===C&&0===x)s=f;else if(-1==s)s--;else if(C>=f+f-262){u.set(u.subarray(f,f+f),0),I-=f,C-=f,k-=f,e=y,r=e;do{n=65535&d[--r],d[r]=f>n?0:n-f}while(0!=--e);e=f,r=e;do{n=65535&h[--r],h[r]=f>n?0:n-f}while(0!=--e);s+=f}if(0===t.He)return;e=t.Le(u,C+x,s),x+=e,3>x||(p=255&u[C],p=(p<x&&0!==t.He)}function oe(e){let t,n,r=A,s=C,i=_;const o=C>f-262?C-(f-262):0;let c=E;const a=l,w=C+258;let d=u[s+i-1],p=u[s+i];B>_||(r>>=2),c>x&&(c=x);do{if(t=e,u[t+i]==p&&u[t+i-1]==d&&u[t]==u[s]&&u[++t]==u[s+1]){s+=2,t++;do{}while(u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&w>s);if(n=258-(w-s),s=w-258,n>i){if(I=e,i=n,n>=c)break;d=u[s+i-1],p=u[s+i]}}}while((e=65535&h[e&a])>o&&0!=--r);return i>x?x:i}e.le=[],e.be=[],e.ae=[],M=[],K=[],P=[],e.de=(t,n)=>{const r=e.ae,s=r[n];let i=n<<1;for(;i<=e.ce&&(i(H||(H=8),L||(L=8),G||(G=0),t.Fe=null,-1==S&&(S=6),1>L||L>9||8!=H||9>I||I>15||0>S||S>9||0>G||G>2?-2:(t.Oe=e,a=I,f=1<(t.qe=t.Ge=0,t.Fe=null,e.pending=0,e.Je=0,n=113,c=0,U.re=M,U.ie=xe.Me,N.re=K,N.ie=xe.Ke,T.re=P,T.ie=xe.Pe,F=0,O=0,j=8,q(),(()=>{w=2*f,d[y-1]=0;for(let e=0;y-1>e;e++)d[e]=0;D=Ve[V].De,B=Ve[V].Ae,E=Ve[V].Ve,A=Ve[V].Re,C=0,k=0,x=0,v=_=2,z=0,p=0})(),0))(t))),e.Qe=()=>42!=n&&113!=n&&666!=n?-2:(e.Te=null,e.Ne=null,e.Ue=null,d=null,h=null,u=null,e.Oe=null,113==n?-3:0),e.Xe=(e,t,n)=>{let r=0;return-1==t&&(t=6),0>t||t>9||0>n||n>2?-2:(Ve[V].Be!=Ve[t].Be&&0!==e.qe&&(r=e.Ye(1)),V!=t&&(V=t,D=Ve[V].De,B=Ve[V].Ae,E=Ve[V].Ve,A=Ve[V].Re),R=n,r)},e.Ze=(e,t,r)=>{let s,i=r,o=0;if(!t||42!=n)return-2;if(3>i)return 0;for(i>f-262&&(i=f-262,o=r-i),u.set(t.subarray(o,o+i),0),C=i,k=i,p=255&u[0],p=(p<=s;s++)p=(p<{let o,w,m,A,B;if(i>4||0>i)return-2;if(!r.$e||!r.et&&0!==r.He||666==n&&4!=i)return r.Fe=Re[4],-2;if(0===r.tt)return r.Fe=Re[7],-5;var E;if(t=r,A=c,c=i,42==n&&(w=8+(a-8<<4)<<8,m=(V-1&255)>>1,m>3&&(m=3),w|=m<<6,0!==C&&(w|=32),w+=31-w%31,n=113,J((E=w)>>8&255),J(255&E)),0!==e.pending){if(t.We(),0===t.tt)return c=-1,0}else if(0===t.He&&A>=i&&4!=i)return t.Fe=Re[7],-5;if(666==n&&0!==t.He)return r.Fe=Re[7],-5;if(0!==t.He||0!==x||0!=i&&666!=n){switch(B=-1,Ve[V].Be){case 0:B=(e=>{let n,r=65535;for(r>s-5&&(r=s-5);;){if(1>=x){if(ie(),0===x&&0==e)return 0;if(0===x)break}if(C+=x,x=0,n=k+r,(0===C||C>=n)&&(x=C-n,C=n,se(!1),0===t.tt))return 0;if(C-k>=f-262&&(se(!1),0===t.tt))return 0}return se(4==e),0===t.tt?4==e?2:0:4==e?3:1})(i);break;case 1:B=(e=>{let n,r=0;for(;;){if(262>x){if(ie(),262>x&&0==e)return 0;if(0===x)break}if(3>x||(p=(p<f-262||2!=R&&(v=oe(r)),3>v)n=ee(0,255&u[C]),x--,C++;else if(n=ee(C-I,v-3),x-=v,v>D||3>x)C+=v,v=0,p=255&u[C],p=(p<{let n,r,s=0;for(;;){if(262>x){if(ie(),262>x&&0==e)return 0;if(0===x)break}if(3>x||(p=(p<_&&f-262>=(C-s&65535)&&(2!=R&&(v=oe(s)),5>=v&&(1==R||3==v&&C-I>4096)&&(v=2)),3>_||v>_)if(0!==z){if(n=ee(0,255&u[C-1]),n&&se(!1),C++,x--,0===t.tt)return 0}else z=1,C++,x--;else{r=C+x-3,n=ee(C-1-S,_-3),x-=_-1,_-=2;do{++C>r||(p=(p<1+j+10-O&&(X(2,3),Y(256,xe._e),$()),j=7;else if(re(0,0,!1),3==i)for(o=0;y>o;o++)d[o]=0;if(t.We(),0===t.tt)return c=-1,0}}return 4!=i?0:1}}function Me(){const e=this;e.nt=0,e.rt=0,e.He=0,e.qe=0,e.tt=0,e.Ge=0}function Ke(e){const t=new Me,n=(o=e&&e.chunkSize?e.chunkSize:65536)+5*(r.floor(o/16383)+1);var o;const c=new i(n);let f=e?e.level:-1;void 0===f&&(f=-1),t.je(f),t.$e=c,this.append=(e,r)=>{let o,f,a=0,l=0,u=0;const w=[];if(e.length){t.nt=0,t.et=e,t.He=e.length;do{if(t.rt=0,t.tt=n,o=t.Ye(0),0!=o)throw new s("deflating: "+t.Fe);t.rt&&(t.rt==n?w.push(new i(c)):w.push(c.slice(0,t.rt))),u+=t.rt,r&&t.nt>0&&t.nt!=a&&(r(t.nt),a=t.nt)}while(t.He>0||0===t.tt);return w.length>1?(f=new i(u),w.forEach((e=>{f.set(e,l),l+=e.length}))):f=w[0]||new i,f}},this.flush=()=>{let e,r,o=0,f=0;const a=[];do{if(t.rt=0,t.tt=n,e=t.Ye(4),1!=e&&0!=e)throw new s("deflating: "+t.Fe);n-t.tt>0&&a.push(c.slice(0,t.rt)),f+=t.rt}while(t.He>0||0===t.tt);return t.Qe(),r=new i(f),a.forEach((e=>{r.set(e,o),o+=e.length})),r}}Me.prototype={je(e,t){const n=this;return n.Oe=new Ee,t||(t=15),n.Oe.je(n,e,t)},Ye(e){const t=this;return t.Oe?t.Oe.Ye(t,e):-2},Qe(){const e=this;if(!e.Oe)return-2;const t=e.Oe.Qe();return e.Oe=null,t},Xe(e,t){const n=this;return n.Oe?n.Oe.Xe(n,e,t):-2},Ze(e,t){const n=this;return n.Oe?n.Oe.Ze(n,e,t):-2},Le(e,t,n){const r=this;let s=r.He;return s>n&&(s=n),0===s?0:(r.He-=s,e.set(r.et.subarray(r.nt,r.nt+s),t),r.nt+=s,r.qe+=s,s)},We(){const e=this;let t=e.Oe.pending;t>e.tt&&(t=e.tt),0!==t&&(e.$e.set(e.Oe.Ue.subarray(e.Oe.Je,e.Oe.Je+t),e.rt),e.rt+=t,e.Oe.Je+=t,e.Ge+=t,e.tt-=t,e.Oe.pending-=t,0===e.Oe.pending&&(e.Oe.Je=0))}};const Pe=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],Ue=[96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,192,80,7,10,0,8,96,0,8,32,0,9,160,0,8,0,0,8,128,0,8,64,0,9,224,80,7,6,0,8,88,0,8,24,0,9,144,83,7,59,0,8,120,0,8,56,0,9,208,81,7,17,0,8,104,0,8,40,0,9,176,0,8,8,0,8,136,0,8,72,0,9,240,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,200,81,7,13,0,8,100,0,8,36,0,9,168,0,8,4,0,8,132,0,8,68,0,9,232,80,7,8,0,8,92,0,8,28,0,9,152,84,7,83,0,8,124,0,8,60,0,9,216,82,7,23,0,8,108,0,8,44,0,9,184,0,8,12,0,8,140,0,8,76,0,9,248,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,196,81,7,11,0,8,98,0,8,34,0,9,164,0,8,2,0,8,130,0,8,66,0,9,228,80,7,7,0,8,90,0,8,26,0,9,148,84,7,67,0,8,122,0,8,58,0,9,212,82,7,19,0,8,106,0,8,42,0,9,180,0,8,10,0,8,138,0,8,74,0,9,244,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,204,81,7,15,0,8,102,0,8,38,0,9,172,0,8,6,0,8,134,0,8,70,0,9,236,80,7,9,0,8,94,0,8,30,0,9,156,84,7,99,0,8,126,0,8,62,0,9,220,82,7,27,0,8,110,0,8,46,0,9,188,0,8,14,0,8,142,0,8,78,0,9,252,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,194,80,7,10,0,8,97,0,8,33,0,9,162,0,8,1,0,8,129,0,8,65,0,9,226,80,7,6,0,8,89,0,8,25,0,9,146,83,7,59,0,8,121,0,8,57,0,9,210,81,7,17,0,8,105,0,8,41,0,9,178,0,8,9,0,8,137,0,8,73,0,9,242,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,202,81,7,13,0,8,101,0,8,37,0,9,170,0,8,5,0,8,133,0,8,69,0,9,234,80,7,8,0,8,93,0,8,29,0,9,154,84,7,83,0,8,125,0,8,61,0,9,218,82,7,23,0,8,109,0,8,45,0,9,186,0,8,13,0,8,141,0,8,77,0,9,250,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,198,81,7,11,0,8,99,0,8,35,0,9,166,0,8,3,0,8,131,0,8,67,0,9,230,80,7,7,0,8,91,0,8,27,0,9,150,84,7,67,0,8,123,0,8,59,0,9,214,82,7,19,0,8,107,0,8,43,0,9,182,0,8,11,0,8,139,0,8,75,0,9,246,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,206,81,7,15,0,8,103,0,8,39,0,9,174,0,8,7,0,8,135,0,8,71,0,9,238,80,7,9,0,8,95,0,8,31,0,9,158,84,7,99,0,8,127,0,8,63,0,9,222,82,7,27,0,8,111,0,8,47,0,9,190,0,8,15,0,8,143,0,8,79,0,9,254,96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,193,80,7,10,0,8,96,0,8,32,0,9,161,0,8,0,0,8,128,0,8,64,0,9,225,80,7,6,0,8,88,0,8,24,0,9,145,83,7,59,0,8,120,0,8,56,0,9,209,81,7,17,0,8,104,0,8,40,0,9,177,0,8,8,0,8,136,0,8,72,0,9,241,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,201,81,7,13,0,8,100,0,8,36,0,9,169,0,8,4,0,8,132,0,8,68,0,9,233,80,7,8,0,8,92,0,8,28,0,9,153,84,7,83,0,8,124,0,8,60,0,9,217,82,7,23,0,8,108,0,8,44,0,9,185,0,8,12,0,8,140,0,8,76,0,9,249,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,197,81,7,11,0,8,98,0,8,34,0,9,165,0,8,2,0,8,130,0,8,66,0,9,229,80,7,7,0,8,90,0,8,26,0,9,149,84,7,67,0,8,122,0,8,58,0,9,213,82,7,19,0,8,106,0,8,42,0,9,181,0,8,10,0,8,138,0,8,74,0,9,245,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,205,81,7,15,0,8,102,0,8,38,0,9,173,0,8,6,0,8,134,0,8,70,0,9,237,80,7,9,0,8,94,0,8,30,0,9,157,84,7,99,0,8,126,0,8,62,0,9,221,82,7,27,0,8,110,0,8,46,0,9,189,0,8,14,0,8,142,0,8,78,0,9,253,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,195,80,7,10,0,8,97,0,8,33,0,9,163,0,8,1,0,8,129,0,8,65,0,9,227,80,7,6,0,8,89,0,8,25,0,9,147,83,7,59,0,8,121,0,8,57,0,9,211,81,7,17,0,8,105,0,8,41,0,9,179,0,8,9,0,8,137,0,8,73,0,9,243,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,203,81,7,13,0,8,101,0,8,37,0,9,171,0,8,5,0,8,133,0,8,69,0,9,235,80,7,8,0,8,93,0,8,29,0,9,155,84,7,83,0,8,125,0,8,61,0,9,219,82,7,23,0,8,109,0,8,45,0,9,187,0,8,13,0,8,141,0,8,77,0,9,251,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,199,81,7,11,0,8,99,0,8,35,0,9,167,0,8,3,0,8,131,0,8,67,0,9,231,80,7,7,0,8,91,0,8,27,0,9,151,84,7,67,0,8,123,0,8,59,0,9,215,82,7,19,0,8,107,0,8,43,0,9,183,0,8,11,0,8,139,0,8,75,0,9,247,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,207,81,7,15,0,8,103,0,8,39,0,9,175,0,8,7,0,8,135,0,8,71,0,9,239,80,7,9,0,8,95,0,8,31,0,9,159,84,7,99,0,8,127,0,8,63,0,9,223,82,7,27,0,8,111,0,8,47,0,9,191,0,8,15,0,8,143,0,8,79,0,9,255],Ne=[80,5,1,87,5,257,83,5,17,91,5,4097,81,5,5,89,5,1025,85,5,65,93,5,16385,80,5,3,88,5,513,84,5,33,92,5,8193,82,5,9,90,5,2049,86,5,129,192,5,24577,80,5,2,87,5,385,83,5,25,91,5,6145,81,5,7,89,5,1537,85,5,97,93,5,24577,80,5,4,88,5,769,84,5,49,92,5,12289,82,5,13,90,5,3073,86,5,193,192,5,24577],Te=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],We=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,112,112],He=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],Le=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];function je(){let e,t,n,r,s,i;function o(e,t,o,c,f,a,l,u,w,h,d){let p,y,m,b,g,k,v,S,z,C,I,x,_,A,D;C=0,g=o;do{n[e[t+C]]++,C++,g--}while(0!==g);if(n[0]==o)return l[0]=-1,u[0]=0,0;for(S=u[0],k=1;15>=k&&0===n[k];k++);for(v=k,k>S&&(S=k),g=15;0!==g&&0===n[g];g--);for(m=g,S>g&&(S=g),u[0]=S,A=1<k;k++,A<<=1)if(0>(A-=n[k]))return-3;if(0>(A-=n[g]))return-3;for(n[g]+=A,i[1]=k=0,C=1,_=2;0!=--g;)i[_]=k+=n[C],_++,C++;g=0,C=0;do{0!==(k=e[t+C])&&(d[i[k]++]=g),C++}while(++g=v;v++)for(p=n[v];0!=p--;){for(;v>x+S;){if(b++,x+=S,D=m-x,D=D>S?S:D,(y=1<<(k=v-x))>p+1&&(y-=p+1,_=v,D>k))for(;++kn[++_];)y-=n[_];if(D=1<1440)return-3;s[b]=I=h[0],h[0]+=D,0!==b?(i[b]=g,r[0]=k,r[1]=S,k=g>>>x-S,r[2]=I-s[b-1]-k,w.set(r,3*(s[b-1]+k))):l[0]=I}for(r[1]=v-x,o>C?d[C]d[C]?0:96,r[2]=d[C++]):(r[0]=a[d[C]-c]+16+64,r[2]=f[d[C++]-c]):r[0]=192,y=1<>>x;D>k;k+=y)w.set(r,3*(I+k));for(k=1<>>=1)g^=k;for(g^=k,z=(1<c;c++)t[c]=0;for(c=0;16>c;c++)n[c]=0;for(c=0;3>c;c++)r[c]=0;s.set(n.subarray(0,15),0),i.set(n.subarray(0,16),0)}this.st=(n,r,s,i,f)=>{let a;return c(19),e[0]=0,a=o(n,0,19,19,null,null,s,r,i,e,t),-3==a?f.Fe="oversubscribed dynamic bit lengths tree":-5!=a&&0!==r[0]||(f.Fe="incomplete dynamic bit lengths tree",a=-3),a},this.it=(n,r,s,i,f,a,l,u,w)=>{let h;return c(288),e[0]=0,h=o(s,0,n,257,Te,We,a,i,u,e,t),0!=h||0===i[0]?(-3==h?w.Fe="oversubscribed literal/length tree":-4!=h&&(w.Fe="incomplete literal/length tree",h=-3),h):(c(288),h=o(s,n,r,0,He,Le,l,f,u,e,t),0!=h||0===f[0]&&n>257?(-3==h?w.Fe="oversubscribed distance tree":-5==h?(w.Fe="incomplete distance tree",h=-3):-4!=h&&(w.Fe="empty distance tree with lengths",h=-3),h):0)}}function Fe(){const e=this;let t,n,r,s,i=0,o=0,c=0,f=0,a=0,l=0,u=0,w=0,h=0,d=0;function p(e,t,n,r,s,i,o,c){let f,a,l,u,w,h,d,p,y,m,b,g,k,v,S,z;d=c.nt,p=c.He,w=o.ot,h=o.ct,y=o.write,m=yh;)p--,w|=(255&c.ft(d++))<>=a[z+1],h-=a[z+1],0!=(16&u)){for(u&=15,k=a[z+2]+(w&Pe[u]),w>>=u,h-=u;15>h;)p--,w|=(255&c.ft(d++))<>=a[z+1],h-=a[z+1],0!=(16&u)){for(u&=15;u>h;)p--,w|=(255&c.ft(d++))<>=u,h-=u,m-=k,v>y){S=y-v;do{S+=o.end}while(0>S);if(u=o.end-S,k>u){if(k-=u,y-S>0&&u>y-S)do{o.lt[y++]=o.lt[S++]}while(0!=--u);else o.lt.set(o.lt.subarray(S,S+u),y),y+=u,S+=u,u=0;S=0}}else S=y-v,y-S>0&&2>y-S?(o.lt[y++]=o.lt[S++],o.lt[y++]=o.lt[S++],k-=2):(o.lt.set(o.lt.subarray(S,S+2),y),y+=2,S+=2,k-=2);if(y-S>0&&k>y-S)do{o.lt[y++]=o.lt[S++]}while(0!=--k);else o.lt.set(o.lt.subarray(S,S+k),y),y+=k,S+=k,k=0;break}if(0!=(64&u))return c.Fe="invalid distance code",k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,-3;f+=a[z+2],f+=w&Pe[u],z=3*(l+f),u=a[z]}break}if(0!=(64&u))return 0!=(32&u)?(k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,1):(c.Fe="invalid literal/length code",k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,-3);if(f+=a[z+2],f+=w&Pe[u],z=3*(l+f),0===(u=a[z])){w>>=a[z+1],h-=a[z+1],o.lt[y++]=a[z+2],m--;break}}else w>>=a[z+1],h-=a[z+1],o.lt[y++]=a[z+2],m--}while(m>=258&&p>=10);return k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,0}e.init=(e,i,o,c,f,a)=>{t=0,u=e,w=i,r=o,h=c,s=f,d=a,n=null},e.ut=(e,y,m)=>{let b,g,k,v,S,z,C,I=0,x=0,_=0;for(_=y.nt,v=y.He,I=e.ot,x=e.ct,S=e.write,z=S=258&&v>=10&&(e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,m=p(u,w,r,h,s,d,e,y),_=y.nt,v=y.He,I=e.ot,x=e.ct,S=e.write,z=Sx;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>>=n[g+1],x-=n[g+1],k=n[g],0===k){f=n[g+2],t=6;break}if(0!=(16&k)){a=15&k,i=n[g+2],t=2;break}if(0==(64&k)){c=k,o=g/3+n[g+2];break}if(0!=(32&k)){t=7;break}return t=9,y.Fe="invalid literal/length code",m=-3,e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);case 2:for(b=a;b>x;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>=b,x-=b,c=w,n=s,o=d,t=3;case 3:for(b=c;b>x;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>=n[g+1],x-=n[g+1],k=n[g],0!=(16&k)){a=15&k,l=n[g+2],t=4;break}if(0==(64&k)){c=k,o=g/3+n[g+2];break}return t=9,y.Fe="invalid distance code",m=-3,e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);case 4:for(b=a;b>x;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>=b,x-=b,t=5;case 5:for(C=S-l;0>C;)C+=e.end;for(;0!==i;){if(0===z&&(S==e.end&&0!==e.read&&(S=0,z=S7&&(x-=8,v++,_--),e.write=S,m=e.wt(y,m),S=e.write,z=S{}}je.dt=(e,t,n,r)=>(e[0]=9,t[0]=5,n[0]=Ue,r[0]=Ne,0);const Oe=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];function qe(e,t){const n=this;let r,s=0,o=0,c=0,a=0;const l=[0],u=[0],w=new Fe;let h=0,d=new f(4320);const p=new je;n.ct=0,n.ot=0,n.lt=new i(t),n.end=t,n.read=0,n.write=0,n.reset=(e,t)=>{t&&(t[0]=0),6==s&&w.ht(e),s=0,n.ct=0,n.ot=0,n.read=n.write=0},n.reset(e,null),n.wt=(e,t)=>{let r,s,i;return s=e.rt,i=n.read,r=(i>n.write?n.end:n.write)-i,r>e.tt&&(r=e.tt),0!==r&&-5==t&&(t=0),e.tt-=r,e.Ge+=r,e.$e.set(n.lt.subarray(i,i+r),s),s+=r,i+=r,i==n.end&&(i=0,n.write==n.end&&(n.write=0),r=n.write-i,r>e.tt&&(r=e.tt),0!==r&&-5==t&&(t=0),e.tt-=r,e.Ge+=r,e.$e.set(n.lt.subarray(i,i+r),s),s+=r,i+=r),e.rt=s,n.read=i,t},n.ut=(e,t)=>{let i,f,y,m,b,g,k,v;for(m=e.nt,b=e.He,f=n.ot,y=n.ct,g=n.write,k=gy;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>1){case 0:f>>>=3,y-=3,i=7&y,f>>>=i,y-=i,s=1;break;case 1:S=[],z=[],C=[[]],I=[[]],je.dt(S,z,C,I),w.init(S[0],z[0],C[0],0,I[0],0),f>>>=3,y-=3,s=6;break;case 2:f>>>=3,y-=3,s=3;break;case 3:return f>>>=3,y-=3,s=9,e.Fe="invalid block type",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t)}break;case 1:for(;32>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>16&65535)!=(65535&f))return s=9,e.Fe="invalid stored block lengths",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);o=65535&f,f=y=0,s=0!==o?2:0!==h?7:0;break;case 2:if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);if(0===k&&(g==n.end&&0!==n.read&&(g=0,k=gb&&(i=b),i>k&&(i=k),n.lt.set(e.Le(m,i),g),m+=i,b-=i,g+=i,k-=i,0!=(o-=i))break;s=0!==h?7:0;break;case 3:for(;14>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<29||(i>>5&31)>29)return s=9,e.Fe="too many length or distance symbols",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);if(i=258+(31&i)+(i>>5&31),!r||r.lengthv;v++)r[v]=0;f>>>=14,y-=14,a=0,s=4;case 4:for(;4+(c>>>10)>a;){for(;3>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>=3,y-=3}for(;19>a;)r[Oe[a++]]=0;if(l[0]=7,i=p.st(r,l,u,d,e),0!=i)return-3==(t=i)&&(r=null,s=9),n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);a=0,s=5;case 5:for(;i=c,258+(31&i)+(i>>5&31)>a;){let o,w;for(i=l[0];i>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<w)f>>>=i,y-=i,r[a++]=w;else{for(v=18==w?7:w-14,o=18==w?11:3;i+v>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>=i,y-=i,o+=f&Pe[v],f>>>=v,y-=v,v=a,i=c,v+o>258+(31&i)+(i>>5&31)||16==w&&1>v)return r=null,s=9,e.Fe="invalid bit length repeat",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);w=16==w?r[v-1]:0;do{r[v++]=w}while(0!=--o);a=v}}if(u[0]=-1,x=[],_=[],A=[],D=[],x[0]=9,_[0]=6,i=c,i=p.it(257+(31&i),1+(i>>5&31),r,x,_,A,D,d,e),0!=i)return-3==i&&(r=null,s=9),t=i,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);w.init(x[0],_[0],d,A[0],d,D[0]),s=6;case 6:if(n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,1!=(t=w.ut(n,e,t)))return n.wt(e,t);if(t=0,w.ht(e),m=e.nt,b=e.He,f=n.ot,y=n.ct,g=n.write,k=g{n.reset(e,null),n.lt=null,d=null},n.yt=(e,t,r)=>{n.lt.set(e.subarray(t,t+r),0),n.read=n.write=r},n.bt=()=>1==s?1:0}const Ge=[0,0,255,255];function Je(){const e=this;function t(e){return e&&e.gt?(e.qe=e.Ge=0,e.Fe=null,e.gt.mode=7,e.gt.kt.reset(e,null),0):-2}e.mode=0,e.method=0,e.vt=[0],e.St=0,e.marker=0,e.zt=0,e.Ct=t=>(e.kt&&e.kt.ht(t),e.kt=null,0),e.It=(n,r)=>(n.Fe=null,e.kt=null,8>r||r>15?(e.Ct(n),-2):(e.zt=r,n.gt.kt=new qe(n,1<{let n,r;if(!e||!e.gt||!e.et)return-2;const s=e.gt;for(t=4==t?-5:0,n=-5;;)switch(s.mode){case 0:if(0===e.He)return n;if(n=t,e.He--,e.qe++,8!=(15&(s.method=e.ft(e.nt++)))){s.mode=13,e.Fe="unknown compression method",s.marker=5;break}if(8+(s.method>>4)>s.zt){s.mode=13,e.Fe="invalid win size",s.marker=5;break}s.mode=1;case 1:if(0===e.He)return n;if(n=t,e.He--,e.qe++,r=255&e.ft(e.nt++),((s.method<<8)+r)%31!=0){s.mode=13,e.Fe="incorrect header check",s.marker=5;break}if(0==(32&r)){s.mode=7;break}s.mode=2;case 2:if(0===e.He)return n;n=t,e.He--,e.qe++,s.St=(255&e.ft(e.nt++))<<24&4278190080,s.mode=3;case 3:if(0===e.He)return n;n=t,e.He--,e.qe++,s.St+=(255&e.ft(e.nt++))<<16&16711680,s.mode=4;case 4:if(0===e.He)return n;n=t,e.He--,e.qe++,s.St+=(255&e.ft(e.nt++))<<8&65280,s.mode=5;case 5:return 0===e.He?n:(n=t,e.He--,e.qe++,s.St+=255&e.ft(e.nt++),s.mode=6,2);case 6:return s.mode=13,e.Fe="need dictionary",s.marker=0,-2;case 7:if(n=s.kt.ut(e,n),-3==n){s.mode=13,s.marker=0;break}if(0==n&&(n=t),1!=n)return n;n=t,s.kt.reset(e,s.vt),s.mode=12;case 12:return e.He=0,1;case 13:return-3;default:return-2}},e._t=(e,t,n)=>{let r=0,s=n;if(!e||!e.gt||6!=e.gt.mode)return-2;const i=e.gt;return s<1<{let n,r,s,i,o;if(!e||!e.gt)return-2;const c=e.gt;if(13!=c.mode&&(c.mode=13,c.marker=0),0===(n=e.He))return-5;for(r=e.nt,s=c.marker;0!==n&&4>s;)e.ft(r)==Ge[s]?s++:s=0!==e.ft(r)?0:4-s,r++,n--;return e.qe+=r-e.nt,e.nt=r,e.He=n,c.marker=s,4!=s?-3:(i=e.qe,o=e.Ge,t(e),e.qe=i,e.Ge=o,c.mode=7,0)},e.Dt=e=>e&&e.gt&&e.gt.kt?e.gt.kt.bt():-2}function Qe(){}function Xe(e){const t=new Qe,n=e&&e.chunkSize?r.floor(2*e.chunkSize):131072,o=new i(n);let c=!1;t.It(),t.$e=o,this.append=(e,r)=>{const f=[];let a,l,u=0,w=0,h=0;if(0!==e.length){t.nt=0,t.et=e,t.He=e.length;do{if(t.rt=0,t.tt=n,0!==t.He||c||(t.nt=0,c=!0),a=t.xt(0),c&&-5===a){if(0!==t.He)throw new s("inflating: bad input")}else if(0!==a&&1!==a)throw new s("inflating: "+t.Fe);if((c||1===a)&&t.He===e.length)throw new s("inflating: bad input");t.rt&&(t.rt===n?f.push(new i(o)):f.push(o.slice(0,t.rt))),h+=t.rt,r&&t.nt>0&&t.nt!=u&&(r(t.nt),u=t.nt)}while(t.He>0||0===t.tt);return f.length>1?(l=new i(h),f.forEach((e=>{l.set(e,w),w+=e.length}))):l=f[0]||new i,l}},this.flush=()=>{t.Ct()}}Qe.prototype={It(e){const t=this;return t.gt=new Je,e||(e=15),t.gt.It(t,e)},xt(e){const t=this;return t.gt?t.gt.xt(t,e):-2},Ct(){const e=this;if(!e.gt)return-2;const t=e.gt.Ct(e);return e.gt=null,t},At(){const e=this;return e.gt?e.gt.At(e):-2},_t(e,t){const n=this;return n.gt?n.gt._t(n,e,t):-2},ft(e){return this.et[e]},Le(e,t){return this.et.subarray(e,e+t)}},self.initCodec=()=>{self.Deflate=Ke,self.Inflate=Xe};\n'],{type:"text/javascript"}));e({workerScripts:{inflate:[t],deflate:[t]}})})(Q),e.BlobReader=It,e.BlobWriter=At,e.Data64URIReader=class extends Ft{constructor(e){super();let t=e.length;for(;"="==e.charAt(t-1);)t--;const r=e.indexOf(",")+1;n.assign(this,{dataURI:e,dataStart:r,size:a.floor(.75*(t-r))})}readUint8Array(e,t){const{dataStart:n,dataURI:r}=this,s=new w(t),i=4*a.floor(e/3),o=atob(r.substring(i+n,4*a.ceil((e+t)/3)+n)),c=e-3*a.floor(i/4);for(let e=c;c+t>e;e++)s[e-c]=o.charCodeAt(e);return s}},e.Data64URIWriter=class extends Et{constructor(e){super(),n.assign(this,{data:"data:"+(e||"")+";base64,",pending:[]})}writeUint8Array(e){const t=this;let n=0,s=t.pending;const i=t.pending.length;for(t.pending="",n=0;n<3*a.floor((i+e.length)/3)-i;n++)s+=r.fromCharCode(e[n]);for(;n2?t.data+=x(s):t.pending=s}getData(){return this.data+x(this.pending)}},e.ERR_BAD_FORMAT=zn,e.ERR_CENTRAL_DIRECTORY_NOT_FOUND=_n,e.ERR_DUPLICATED_NAME=Gn,e.ERR_ENCRYPTED=Fn,e.ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUND=Cn,e.ERR_EOCDR_NOT_FOUND=xn,e.ERR_EOCDR_ZIP64_NOT_FOUND=vn,e.ERR_EXTRAFIELD_ZIP64_NOT_FOUND=Rn,e.ERR_HTTP_RANGE=xt,e.ERR_INVALID_COMMENT=jn,e.ERR_INVALID_ENCRYPTION_STRENGTH=er,e.ERR_INVALID_ENTRY_COMMENT=Jn,e.ERR_INVALID_ENTRY_NAME=Qn,e.ERR_INVALID_EXTRAFIELD_DATA=nr,e.ERR_INVALID_EXTRAFIELD_TYPE=tr,e.ERR_INVALID_PASSWORD=ue,e.ERR_INVALID_SIGNATURE=fe,e.ERR_INVALID_VERSION=$n,e.ERR_ITERATOR_COMPLETED_TOO_SOON=vt,e.ERR_LOCAL_FILE_HEADER_NOT_FOUND=Dn,e.ERR_SPLIT_ZIP_FILE=An,e.ERR_UNSUPPORTED_COMPRESSION=In,e.ERR_UNSUPPORTED_ENCRYPTION=En,e.ERR_UNSUPPORTED_FORMAT=rr,e.HttpRangeReader=class extends Yt{constructor(e,t={}){t.useRangeHeader=!0,super(e,t)}},e.HttpReader=Yt,e.Reader=Ft,e.SplitDataReader=Zt,e.SplitDataWriter=Xt,e.SplitZipReader=$t,e.SplitZipWriter=en,e.TextReader=class extends It{constructor(e){super(new m([e],{type:"text/plain"}))}},e.TextWriter=class extends At{constructor(e){super(e),n.assign(this,{encoding:e,utf8:!e||"utf-8"==e.toLowerCase()})}async getData(){const{encoding:e,utf8:t}=this,r=await super.getData();if(r.text&&t)return r.text();{const t=new FileReader;return new y(((s,i)=>{n.assign(t,{onload:({target:e})=>s(e.result),onerror:()=>i(t.error)}),t.readAsText(r,e)}))}}},e.Uint8ArrayReader=class extends Ft{constructor(e){super(),n.assign(this,{array:e,size:e.length})}readUint8Array(e,t){return this.array.slice(e,e+t)}},e.Uint8ArrayWriter=class extends Et{init(e=0){super.init(),n.assign(this,{offset:0,array:new w(e)})}writeUint8Array(e){const t=this;if(t.offset+e.length>t.array.length){const n=t.array;t.array=new w(n.length+e.length),t.array.set(n)}t.array.set(e,t.offset),t.offset+=e.length}getData(){return this.array}},e.Writer=Et,e.ZipReader=class{constructor(e,t={}){n.assign(this,{reader:jt(e),options:t,config:j()})}async*getEntriesGenerator(e={}){const t=this;let{reader:r}=t;const{config:s}=t;if(await Gt(r),r.size!==V&&r.readUint8Array||(r=new It(await new u(r.readable).blob()),await Gt(r)),22>r.size)throw new d(zn);r.chunkSize=J(s);const i=await(async(e,t,n)=>{const r=new w(4);return Xn(r).setUint32(0,101010256,!0),await s(22)||await s(a.min(1048582,n));async function s(t){const s=n-t,i=await Qt(e,s,t);for(let e=i.length-22;e>=0;e--)if(i[e]==r[0]&&i[e+1]==r[1]&&i[e+2]==r[2]&&i[e+3]==r[3])return{offset:s+e,buffer:i.slice(e,e+22).buffer}}})(r,0,r.size);if(!i)throw Yn(Xn(await Qt(r,0,4)))==H?new d(An):new d(xn);const o=Xn(i);let c=Yn(o,12),l=Yn(o,16);const f=i.offset,h=Kn(o,20),p=f+22+h;let g=Kn(o,4);const m=r.lastDiskNumber||0;let b=Kn(o,6),S=Kn(o,8),k=0,z=0;if(l==I||c==I||S==A||b==A){const e=Xn(await Qt(r,i.offset-20,20));if(Yn(e,0)!=N)throw new d(vn);l=Zn(e,8);let t=await Qt(r,l,56,-1),n=Xn(t);const s=i.offset-20-56;if(Yn(n,0)!=U&&l!=s){const e=l;l=s,k=l-e,t=await Qt(r,l,56,-1),n=Xn(t)}if(Yn(n,0)!=U)throw new d(Cn);g==A&&(g=Yn(n,16)),b==A&&(b=Yn(n,20)),S==A&&(S=Zn(n,32)),c==I&&(c=Zn(n,40)),l-=c}if(m!=g)throw new d(An);if(0>l||l>=r.size)throw new d(zn);let x=0,v=await Qt(r,l,c,b),C=Xn(v);if(c){const e=i.offset-c;if(Yn(C,x)!=O&&l!=e){const t=l;l=e,k=l-t,v=await Qt(r,l,c,b),C=Xn(v)}}if(0>l||l>=r.size)throw new d(zn);const _=Mn(t,e,"filenameEncoding"),D=Mn(t,e,"commentEncoding");for(let i=0;S>i;i++){const o=new Nn(r,s,t.options);if(Yn(C,x)!=O)throw new d(_n);Wn(o,C,x+6);const c=!!o.bitFlag.languageEncodingFlag,l=x+46,u=l+o.filenameLength,f=u+o.extraFieldLength,w=Kn(C,x+4),h=0==(0&w),p=v.subarray(l,u),g=Kn(C,x+32),m=f+g,b=v.subarray(f,m),R=c,F=c,E=h&&16==(16&Bn(C,x+38)),I=Yn(C,x+42)+k;n.assign(o,{versionMadeBy:w,msDosCompatible:h,compressedSize:0,uncompressedSize:0,commentLength:g,directory:E,offset:I,diskNumberStart:Kn(C,x+34),internalFileAttribute:Kn(C,x+36),externalFileAttribute:Yn(C,x+38),rawFilename:p,filenameUTF8:R,commentUTF8:F,rawExtraField:v.subarray(u,f)});const[A,T]=await y.all([nn(p,R?Tn:_||Hn),nn(b,F?Tn:D||Hn)]);n.assign(o,{rawComment:b,filename:A,comment:T,directory:E||A.endsWith(L)}),z=a.max(I,z),await qn(o,o,C,x+6);const H=new kn(o);H.getData=(e,t)=>o.getData(e,H,t),x=m;const{onprogress:U}=e;if(U)try{await U(i+1,S,new kn(o))}catch(e){}yield H}const R=Mn(t,e,"extractPrependedData"),F=Mn(t,e,"extractAppendedData");return R&&(t.prependedData=z>0?await Qt(r,0,z):new w),t.comment=h?await Qt(r,f+22,h):new w,F&&(t.appendedData=par.push(e)));try{if(e=e.trim(),c.filenames.has(e))throw new d(Gn);return c.filenames.add(e),f=(async(e,r,s,c)=>{r=r.trim(),c.directory&&!r.endsWith(L)?r+=L:c.directory=r.endsWith(L);const l=se(r);if(gr(l)>A)throw new d(Qn);const u=c.comment||"",f=se(u);if(gr(f)>A)throw new d(Jn);const m=lr(e,c,"version",20);if(m>A)throw new d($n);const b=lr(e,c,"versionMadeBy",20);if(b>A)throw new d($n);const S=lr(e,c,dn,new o),k=lr(e,c,hn),z=lr(e,c,pn),x=lr(e,c,yn,!0),v=lr(e,c,gn,0),C=lr(e,c,mn,0),_=lr(e,c,"password"),D=lr(e,c,"encryptionStrength",3),R=lr(e,c,"zipCrypto"),F=lr(e,c,"extendedTimestamp",!0),E=lr(e,c,"keepOrder",!0),O=lr(e,c,"level"),U=lr(e,c,"useWebWorkers"),N=lr(e,c,"bufferedWrite"),B=lr(e,c,"dataDescriptorSignature",!1),K=lr(e,c,"signal"),Y=lr(e,c,"useCompressionStream");let Z=lr(e,c,"dataDescriptor",!0),X=lr(e,c,bn);if(_!==V&&D!==V&&(1>D||D>3))throw new d(er);let G=new w;const{extraField:j}=c;if(j){let e=0,t=0;j.forEach((t=>e+=4+gr(t))),G=new w(e),j.forEach(((e,n)=>{if(n>A)throw new d(tr);if(gr(e)>A)throw new d(nr);hr(G,new h([n]),t),hr(G,new h([gr(e)]),t+2),hr(G,e,t+4),t+=4+gr(e)}))}let Q=0,$=0,ee=0;const te=!0===X;s&&(s=jt(s),await Gt(s),s.size===V?(Z=!0,(X||X===V)&&(X=!0,Q=I)):(ee=s.size,Q=(e=>e+5*(a.floor(e/16383)+1))(ee)));const{diskOffset:ne,diskNumber:re,maxSize:ie}=e.writer,ae=te||ee>=I,oe=te||Q>=I,ce=te||e.offset+e.pendingEntriesSize-ne>=I,le=lr(e,c,"supportZip64SplitFile",!0)&&te||re+a.ceil(e.pendingEntriesSize/ie)>=A;if(ce||ae||oe||le){if(!1===X||!E)throw new d(rr);X=!0}X=X||!1;const ue=(e=>{const{rawFilename:t,lastModDate:n,lastAccessDate:r,creationDate:s,password:i,level:o,zip64:c,zipCrypto:l,dataDescriptor:u,directory:f,rawExtraField:d,encryptionStrength:h,extendedTimestamp:g}=e,m=0!==o&&!f,y=!(!i||!gr(i));let b,S,k,z=e.version;if(y&&!l){b=new w(gr(sr)+2);const e=pr(b);fr(e,0,39169),hr(b,sr,2),ur(e,8,h)}else b=new w;if(g){k=new w(9+(r?4:0)+(s?4:0));const e=pr(k);fr(e,0,W),fr(e,2,gr(k)-4),ur(e,4,1+(r?2:0)+(s?4:0)),dr(e,5,a.floor(n.getTime()/1e3)),r&&dr(e,9,a.floor(r.getTime()/1e3)),s&&dr(e,13,a.floor(s.getTime()/1e3));try{S=new w(36);const e=pr(S),t=cr(n);fr(e,0,10),fr(e,2,32),fr(e,8,1),fr(e,10,24),wr(e,12,t),wr(e,20,cr(r)||t),wr(e,28,cr(s)||t)}catch(e){S=new w}}else S=k=new w;let x=q;u&&(x|=8);let v=0;m&&(v=8),c&&(z=z>45?z:45),y&&(x|=1,l||(z=z>51?z:51,v=99,m&&(b[9]=8)));const C=new w(26),_=pr(C);fr(_,0,z),fr(_,2,x),fr(_,4,v);const D=new p(1),R=pr(D);let F;F=P>n?P:n>M?M:n,fr(R,0,(F.getHours()<<6|F.getMinutes())<<5|F.getSeconds()/2),fr(R,2,(F.getFullYear()-1980<<4|F.getMonth()+1)<<5|F.getDate());const E=D[0];dr(_,6,E),fr(_,22,gr(t));const I=gr(b,k,S,d);fr(_,24,I);const A=new w(30+gr(t)+I);return dr(pr(A),0,T),hr(A,C,4),hr(A,t,30),hr(A,b,30+gr(t)),hr(A,k,30+gr(t,b)),hr(A,S,30+gr(t,b,k)),hr(A,d,30+gr(t,b,k,S)),{localHeaderArray:A,headerArray:C,headerView:_,lastModDate:n,rawLastModDate:E,encrypted:y,compressed:m,version:z,compressionMethod:v,rawExtraFieldExtendedTimestamp:k,rawExtraFieldNTFS:S,rawExtraFieldAES:b}})(c=n.assign({},c,{rawFilename:l,rawComment:f,version:m,versionMadeBy:b,lastModDate:S,lastAccessDate:k,creationDate:z,rawExtraField:G,zip64:X,zip64UncompressedSize:ae,zip64CompressedSize:oe,zip64Offset:ce,zip64DiskNumberStart:le,password:_,level:O,useWebWorkers:U,encryptionStrength:D,extendedTimestamp:F,zipCrypto:R,bufferedWrite:N,keepOrder:E,dataDescriptor:Z,dataDescriptorSignature:B,signal:K,msDosCompatible:x,internalFileAttribute:v,externalFileAttribute:C,useCompressionStream:Y})),fe=(e=>{const{zip64:t,dataDescriptor:n,dataDescriptorSignature:r}=e;let s,i=new w,a=0;return n&&(i=new w(t?r?24:20:r?16:12),s=pr(i),r&&(a=4,dr(s,0,134695760))),{dataDescriptorArray:i,dataDescriptorView:s,dataDescriptorOffset:a}})(c);let de;$=gr(ue.localHeaderArray,fe.dataDescriptorArray)+Q,e.pendingEntriesSize+=$;try{de=await(async(e,r,s,a,o)=>{const{files:c,writer:l}=e,{keepOrder:u,dataDescriptor:f,signal:h}=o,{headerInfo:p}=a,m=t.from(c.values()).pop();let b,S,k,z,x,v,C={};c.set(r,C);try{let t;u&&(t=m&&m.lock,C.lock=new y((e=>k=e))),o.bufferedWrite||e.writerLocked||e.bufferedWrites&&u||!f?(v=new At,v.writable.size=0,b=!0,e.bufferedWrites++,await Gt(l)):(v=l,await _()),await Gt(v);const{writable:p}=l;let{diskOffset:R}=l;if(e.addSplitZipSignature){delete e.addSplitZipSignature;const t=new w(4);dr(pr(t),0,H),await or(p,t),e.offset+=4}b||(await t,await D(p));const{diskNumber:F}=l;if(x=!0,C.diskNumberStart=F,C=await(async(e,t,{diskNumberStart:r,lock:s},a,o,c)=>{const{headerInfo:l,dataDescriptorInfo:u}=a,{localHeaderArray:f,headerArray:d,lastModDate:h,rawLastModDate:p,encrypted:g,compressed:m,version:y,compressionMethod:b,rawExtraFieldExtendedTimestamp:S,rawExtraFieldNTFS:k,rawExtraFieldAES:z}=l,{dataDescriptorArray:x}=u,{rawFilename:v,lastAccessDate:C,creationDate:_,password:D,level:R,zip64:F,zip64UncompressedSize:E,zip64CompressedSize:A,zip64Offset:T,zip64DiskNumberStart:H,zipCrypto:O,dataDescriptor:U,directory:N,versionMadeBy:W,rawComment:q,rawExtraField:L,useWebWorkers:M,onstart:P,onprogress:B,onend:K,signal:Y,encryptionStrength:Z,extendedTimestamp:X,msDosCompatible:G,internalFileAttribute:j,externalFileAttribute:Q,useCompressionStream:$}=c,ee={lock:s,versionMadeBy:W,zip64:F,directory:!!N,filenameUTF8:!0,rawFilename:v,commentUTF8:!0,rawComment:q,rawExtraFieldExtendedTimestamp:S,rawExtraFieldNTFS:k,rawExtraFieldAES:z,rawExtraField:L,extendedTimestamp:X,msDosCompatible:G,internalFileAttribute:j,externalFileAttribute:Q,diskNumberStart:r};let te,ne=0,re=0;const{writable:se}=t;if(e){e.chunkSize=J(o),await or(se,f);const t=e.readable,n=t.size=e.size,r={options:{codecType:st,level:R,password:D,encryptionStrength:Z,zipCrypto:g&&O,passwordVerification:g&&O&&p>>8&255,signed:!0,compressed:m,encrypted:g,useWebWorkers:M,useCompressionStream:$,transferStreams:!1},config:o,streamOptions:{signal:Y,size:n,onstart:P,onprogress:B,onend:K}},s=await St({readable:t,writable:se},r);se.size+=s.size,te=s.signature,re=e.size=t.size,ne=s.size}else await or(se,f);let ie;if(F){let e=4;E&&(e+=8),A&&(e+=8),T&&(e+=8),H&&(e+=4),ie=new w(e)}else ie=new w;return e&&((e,t)=>{const{signature:n,rawExtraFieldZip64:r,compressedSize:s,uncompressedSize:a,headerInfo:o,dataDescriptorInfo:c}=e,{headerView:l,encrypted:u}=o,{dataDescriptorView:f,dataDescriptorOffset:d}=c,{zip64:w,zip64UncompressedSize:h,zip64CompressedSize:p,zipCrypto:g,dataDescriptor:m}=t;if(u&&!g||n===V||(dr(l,10,n),m&&dr(f,d,n)),w){const e=pr(r);fr(e,0,1),fr(e,2,r.length-4);let t=4;h&&(dr(l,18,I),wr(e,t,i(a)),t+=8),p&&(dr(l,14,I),wr(e,t,i(s))),m&&(wr(f,d+4,i(s)),wr(f,d+12,i(a)))}else dr(l,14,s),dr(l,18,a),m&&(dr(f,d+4,s),dr(f,d+8,a))})({signature:te,rawExtraFieldZip64:ie,compressedSize:ne,uncompressedSize:re,headerInfo:l,dataDescriptorInfo:u},c),U&&await or(se,x),n.assign(ee,{uncompressedSize:re,compressedSize:ne,lastModDate:h,rawLastModDate:p,creationDate:_,lastAccessDate:C,encrypted:g,length:gr(f,x)+ne,compressionMethod:b,version:y,headerArray:d,signature:te,rawExtraFieldZip64:ie,zip64UncompressedSize:E,zip64CompressedSize:A,zip64Offset:T,zip64DiskNumberStart:H}),ee})(s,v,C,a,e.config,o),x=!1,c.set(r,C),C.filename=r,b){await v.writable.close();let e=await v.getData();await t,await _(),z=!0,f||(e=await(async(e,t,n,{zipCrypto:r})=>{const s=await(e=>e.slice(0,26).arrayBuffer())(t),i=new g(s);return e.encrypted&&!r||dr(i,14,e.signature),e.zip64?(dr(i,18,I),dr(i,22,I)):(dr(i,18,e.compressedSize),dr(i,22,e.uncompressedSize)),await or(n,new w(s)),t.slice(s.byteLength)})(C,e,p,o)),await D(p),C.diskNumberStart=l.diskNumber,R=l.diskOffset,await e.stream().pipeTo(p,{preventClose:!0,signal:h}),p.size+=e.size,z=!1}if(C.offset=e.offset-R,C.zip64)((e,t)=>{const{rawExtraFieldZip64:n,offset:r,diskNumberStart:s}=e,{zip64UncompressedSize:a,zip64CompressedSize:o,zip64Offset:c,zip64DiskNumberStart:l}=t,u=pr(n);let f=4;a&&(f+=8),o&&(f+=8),c&&(wr(u,f,i(r)),f+=8),l&&dr(u,f,s)})(C,o);else if(C.offset>=I)throw new d(rr);return e.offset+=C.length,C}catch(t){throw(b&&z||!b&&x)&&(e.hasCorruptedEntries=!0,t&&(t.corruptedEntry=!0),b?e.offset+=v.writable.size:e.offset=v.writable.size),c.delete(r),t}finally{b&&e.bufferedWrites--,k&&k(),S&&S()}async function _(){e.writerLocked=!0;const{lockWriter:t}=e;e.lockWriter=new y((t=>S=()=>{e.writerLocked=!1,t()})),await t}async function D(e){p.localHeaderArray.length>l.availableSize&&(l.availableSize=0,await or(e,new w))}})(e,r,s,{headerInfo:ue,dataDescriptorInfo:fe},c)}finally{e.pendingEntriesSize-=$}return n.assign(de,{name:r,comment:u,extraField:j}),new kn(de)})(c,e,r,s),l.add(f),await f}catch(t){throw c.filenames.delete(e),t}finally{l.delete(f);const e=ar.shift();e?e():ir--}}async close(e=new w,n={}){const{pendingAddFileCalls:r,writer:s}=this,{writable:o}=s;for(;r.size;)await y.all(t.from(r));return await(async(e,n,r)=>{const{files:s,writer:o}=e,{diskOffset:c,writable:l}=o;let{diskNumber:u}=o,f=0,h=0,p=e.offset-c,g=s.size;for(const[,{rawFilename:e,rawExtraFieldZip64:t,rawExtraFieldAES:n,rawExtraField:r,rawComment:i,rawExtraFieldExtendedTimestamp:a,rawExtraFieldNTFS:o}]of s)h+=46+gr(e,i,t,n,a,o,r);const m=new w(h),y=pr(m);await Gt(o);let b=0;for(const[e,n]of t.from(s.values()).entries()){const{offset:t,rawFilename:i,rawExtraFieldZip64:c,rawExtraFieldAES:u,rawExtraFieldNTFS:d,rawExtraField:h,rawComment:p,versionMadeBy:g,headerArray:S,directory:k,zip64:z,zip64UncompressedSize:x,zip64CompressedSize:v,zip64DiskNumberStart:C,zip64Offset:_,msDosCompatible:D,internalFileAttribute:R,externalFileAttribute:F,extendedTimestamp:E,lastModDate:T,diskNumberStart:H,uncompressedSize:U,compressedSize:N}=n;let q;if(E){q=new w(9);const e=pr(q);fr(e,0,W),fr(e,2,gr(q)-4),ur(e,4,1),dr(e,5,a.floor(T.getTime()/1e3))}else q=new w;const L=gr(c,u,q,d,h);dr(y,f,O),fr(y,f+4,g);const M=pr(S);x||dr(M,18,U),v||dr(M,14,N),hr(m,S,f+6),fr(y,f+30,L),fr(y,f+32,gr(p)),fr(y,f+34,z&&C?A:H),fr(y,f+36,R),F?dr(y,f+38,F):k&&D&&ur(y,f+38,16),dr(y,f+42,z&&_?I:t),hr(m,i,f+46),hr(m,c,f+46+gr(i)),hr(m,u,f+46+gr(i,c)),hr(m,q,f+46+gr(i,c,u)),hr(m,d,f+46+gr(i,c,u,q)),hr(m,h,f+46+gr(i,c,u,q,d)),hr(m,p,f+46+gr(i)+L);const P=46+gr(i,p)+L;if(f-b>o.availableSize&&(o.availableSize=0,await or(l,m.slice(b,f)),b=f),f+=P,r.onprogress)try{await r.onprogress(e+1,s.size,new kn(n))}catch(e){}}await or(l,b?m.slice(b):m);let S=o.diskNumber;const{availableSize:k}=o;22>k&&S++;let z=lr(e,r,"zip64");if(!(I>p&&I>h&&A>g&&A>S)){if(!1===z)throw new d(rr);z=!0}const x=new w(z?98:22),v=pr(x);f=0,z&&(dr(v,0,U),wr(v,4,i(44)),fr(v,12,45),fr(v,14,45),dr(v,16,S),dr(v,20,u),wr(v,24,i(g)),wr(v,32,i(g)),wr(v,40,i(h)),wr(v,48,i(p)),dr(v,56,N),wr(v,64,i(p)+i(h)),dr(v,72,S+1),lr(e,r,"supportZip64SplitFile",!0)&&(S=A,u=A),g=A,p=I,h=I,f+=76),dr(v,f,101010256),fr(v,f+4,S),fr(v,f+6,u),fr(v,f+8,g),fr(v,f+10,g),dr(v,f+12,h),dr(v,f+16,p);const C=gr(n);if(C){if(C>A)throw new d(jn);fr(v,f+20,C)}await or(l,x),C&&await or(l,n)})(this,e,n),lr(this,n,"preventClose")||await o.close(),s.getData?s.getData():o}},e.configure=Q,e.getMimeType=()=>"application/octet-stream",e.initReader=jt,e.initShimAsyncCodec=(e,t={},n)=>({Deflate:ee(e.Deflate,t.deflate,n),Inflate:ee(e.Inflate,t.inflate,n)}),e.initStream=Gt,e.initWriter=Jt,e.readUint8Array=Qt,e.terminateWorkers=()=>{mt.forEach((e=>{kt(e),e.terminate()}))},n.defineProperty(e,"__esModule",{value:!0})})); diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..9c1eb90 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,148 @@ +const cacheVersion = 'v1.7.3'; +const cacheTitle = `moreload-cache-${cacheVersion}`; +const urlsToCache = [ + 'index.html', + './', + 'styles.css', + 'scripts/network.js', + 'scripts/ui.js', + 'scripts/util.js', + 'scripts/qrcode.js', + 'scripts/zip.min.js', + 'scripts/NoSleep.min.js', + 'scripts/theme.js', + 'sounds/blop.mp3', + 'images/favicon-96x96.png', + 'images/favicon-96x96-notification.png', + 'images/android-chrome-192x192.png', + 'images/android-chrome-192x192-maskable.png', + 'images/android-chrome-512x512.png', + 'images/android-chrome-512x512-maskable.png', + 'images/apple-touch-icon.png', +]; + +self.addEventListener('install', function(event) { + // Perform install steps + event.waitUntil( + caches.open(cacheTitle) + .then(function(cache) { + return cache.addAll(urlsToCache).then(_ => { + console.log('All files cached.'); + }); + }) + ); +}); + +// fetch the resource from the network +const fromNetwork = (request, timeout) => + new Promise((fulfill, reject) => { + const timeoutId = setTimeout(reject, timeout); + fetch(request).then(response => { + clearTimeout(timeoutId); + fulfill(response); + update(request); + }, reject); + }); + +// fetch the resource from the browser cache +const fromCache = request => + caches + .open(cacheTitle) + .then(cache => + cache + .match(request) + .then(matching => matching || cache.match('/offline/')) + ); + +// cache the current page to make it available for offline +const update = request => + caches + .open(cacheTitle) + .then(cache => + fetch(request).then(response => { + cache.put(request, response).then(_ => { + console.log("Page successfully cached.") + }) + }) + ); + +// general strategy when making a request (eg if online try to fetch it +// from the network with a timeout, if something fails serve from cache) +self.addEventListener('fetch', function(event) { + if (event.request.method === "POST") { + // Requests related to Web Share Target. + event.respondWith((async () => { + let share_url = await evaluateRequestData(event.request); + share_url = event.request.url + share_url; + return Response.redirect(encodeURI(share_url), 302); + })()); + } else { + // Regular requests not related to Web Share Target. + event.respondWith( + fromNetwork(event.request, 10000).catch(() => fromCache(event.request)) + ); + event.waitUntil(update(event.request)); + } +}); + + +// on activation, we clean up the previously registered service workers +self.addEventListener('activate', evt => + evt.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== cacheTitle) { + return caches.delete(cacheName); + } + }) + ); + }) + ) +); + +const evaluateRequestData = async function (request) { + const formData = await request.formData(); + const title = formData.get("title"); + const text = formData.get("text"); + const url = formData.get("url"); + const files = formData.getAll("allfiles"); + + + return new Promise(async (resolve) => { + if (files && files.length > 0) { + let fileObjects = []; + for (let i=0; i { + const db = e.target.result; + for (let i = 0; i < fileObjects.length; i++) { + const transaction = db.transaction('share_target_files', 'readwrite'); + const objectStore = transaction.objectStore('share_target_files'); + + const objectStoreRequest = objectStore.add(fileObjects[i]); + objectStoreRequest.onsuccess = _ => { + if (i === fileObjects.length - 1) resolve('?share-target=files'); + } + } + } + DBOpenRequest.onerror = _ => { + resolve(''); + } + } else { + let share_url = '?share-target=text'; + + if (title) share_url += `&title=${title}`; + if (text) share_url += `&text=${text}`; + if (url) share_url += `&url=${url}`; + + resolve(share_url); + } + }); +} diff --git a/public/sounds/blop.mp3 b/public/sounds/blop.mp3 new file mode 100644 index 0000000..28a6244 Binary files /dev/null and b/public/sounds/blop.mp3 differ diff --git a/public/sounds/blop.ogg b/public/sounds/blop.ogg new file mode 100644 index 0000000..d1ce0c2 Binary files /dev/null and b/public/sounds/blop.ogg differ diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..7f2913b --- /dev/null +++ b/public/styles.css @@ -0,0 +1,1457 @@ +/* Constants */ + +:root { + --icon-size: 24px; + --primary-color: #4285f4; + --paired-device-color: #00a69c; + --peer-width: 120px; + color-scheme: light dark; +} + +/* Layout */ + +html, +body { + margin: 0; + display: flex; + flex-direction: column; + width: 100vw; + overflow-x: hidden; + overscroll-behavior: none; + overflow-y: hidden; + /* Only allow selection on message and pair key */ + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +body { + height: 100%; + /* mobile viewport bug fix */ + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; +} + +html { + height: 100%; + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; +} + +.row-reverse { + display: flex; + flex-direction: row-reverse; +} + +.space-between { + justify-content: space-between; +} + +.row { + display: flex; + justify-content: center; + flex-direction: row; +} + +.column { + display: flex; + flex-direction: column; +} + +.center { + display: flex; + align-items: center; + justify-content: center; +} + +.grow { + flex-grow: 1; +} + +.full { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +header { + position: absolute; + align-items: baseline; + padding: 8px 16px; + box-sizing: border-box; + width: 100vw; + z-index: 2; + top: 0; + right: 0; +} + +header > a, +header > div { + margin-left: 8px; +} + +header > div { + display: flex; + flex-direction: column; + align-self: flex-start; + touch-action: manipulation; +} + +header > div .icon-button { + height: 40px; + transition: all 300ms; +} + +header > div > div { + display: flex; + flex-direction: column; +} + +header > div:not(:hover) .icon-button:not(.selected) { + height: 0; + opacity: 0; +} + +#theme-wrapper:hover::before { + border-radius: 20px; + background: currentColor; + opacity: 0.1; + transition: opacity 300ms; + content: ''; + position: absolute; + width: 40px; + top: 0; + bottom: 0; + margin-top: 8px; + margin-bottom: 8px; +} + +header > div:hover .icon-button.selected::before { + opacity: 0.1; +} + +@media (pointer: coarse) { + header > div:hover .icon-button.selected:hover::before { + opacity: 0.2; + } + + header > div .icon-button:not(.selected) { + height: 0; + opacity: 0; + pointer-events: none; + } + + header > div > div { + flex-direction: column-reverse; + } +} + +[hidden] { + display: none !important; +} + + +/* Typography */ + +body { + font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1 { + font-size: 34px; + font-weight: 400; + letter-spacing: -.01em; + line-height: 40px; + margin: 8px 0 0; +} + +h2 { + font-size: 24px; + font-weight: 400; + letter-spacing: -.012em; + line-height: 32px; + color: var(--primary-color);} + +h3 { + font-size: 20px; + font-weight: 500; + margin: 16px 0; + color: var(--primary-color); +} + +.font-subheading { + font-size: 14px; + font-weight: 400; + line-height: 18px; + word-break: normal; +} + +.text-center { + text-align: center; +} + +.font-body1, +body { + font-size: 14px; + font-weight: 400; + line-height: 20px; +} + +.font-body2 { + font-size: 12px; + line-height: 18px; +} + +a, +.icon-button { + text-decoration: none; + color: currentColor; + cursor: pointer; +} + +hr { + color: white; +} + +input { + cursor: pointer; +} + +input[type="checkbox"] { + min-width: 13px; +} + +x-noscript { + background: var(--primary-color); + color: white; + z-index: 2; +} + + +/* Icons */ + +.icon { + width: var(--icon-size); + height: var(--icon-size); + fill: currentColor; +} + + + +/* Shadows */ + +[shadow="1"] { + box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), + 0 1px 8px 0 rgba(0, 0, 0, 0.12), + 0 3px 3px -2px rgba(0, 0, 0, 0.4); +} + +[shadow="2"] { + box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), + 0 1px 10px 0 rgba(0, 0, 0, 0.12), + 0 2px 4px -1px rgba(0, 0, 0, 0.4); +} + + + + +/* Animations */ + +@keyframes fade-in { + 0% { + opacity: 0; + } +} + +#center { + position: relative; + display: flex; + margin-top: 56px; + flex-direction: column-reverse; + flex-grow: 1; + --footer-height: 132px; + max-height: calc(100vh - 56px - var(--footer-height)); + justify-content: space-around; + align-items: center; + overflow-x: hidden; + overflow-y: scroll; + overscroll-behavior-x: none; +} + +@media screen and (max-width: 425px) { + header:has(#edit-pair-devices:not([hidden]))~#center { + --footer-height: 150px; + } +} + +/* Peers List */ + +#x-peers-filler { + display: flex; + flex-grow: 1; +} + +x-peers { + position: relative; + display: flex; + flex-flow: row wrap; + flex-grow: 1; + align-items: start !important; + justify-content: center; + + z-index: 2; + transition: --bg-color 0.5s ease; + overflow-y: scroll; + overflow-x: hidden; + overscroll-behavior-x: none; + scrollbar-width: none; + + --peers-per-row: 6; /* default if browser does not support :has selector */ + --x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px)); + width: var(--x-peers-width); + margin-right: 20px; + margin-left: 20px; +} + +x-peers.overflowing { + background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)), + linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%, + /* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)), + radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%; + + background-repeat: no-repeat; + background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +x-peers:has(> x-peer) { + --peers-per-row: 10; +} + +@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px), +screen and (min-height: 486px) and (max-height: 631px) and (min-width: 426px) { + x-peers:has(> x-peer) { + --peers-per-row: 3; + } + + x-peers:has(> x-peer:nth-of-type(7)) { + --peers-per-row: 4; + } + + x-peers:has(> x-peer:nth-of-type(10)) { + --peers-per-row: 5; + } + + x-peers:has(> x-peer:nth-of-type(13)) { + --peers-per-row: 6; + } + + x-peers:has(> x-peer:nth-of-type(16)) { + --peers-per-row: 7; + } + + x-peers:has(> x-peer:nth-of-type(19)) { + --peers-per-row: 8; + } + + x-peers:has(> x-peer:nth-of-type(22)) { + --peers-per-row: 9; + } + + x-peers:has(> x-peer:nth-of-type(25)) { + --peers-per-row: 10; + } +} + +@media screen and (min-height: 649px) and (max-width: 425px), +screen and (min-height: 631px) and (min-width: 426px) { + x-peers:has(> x-peer) { + --peers-per-row: 3; + } + + x-peers:has(> x-peer:nth-of-type(10)) { + --peers-per-row: 4; + } + + x-peers:has(> x-peer:nth-of-type(13)) { + --peers-per-row: 5; + } + + x-peers:has(> x-peer:nth-of-type(16)) { + --peers-per-row: 6; + } + + x-peers:has(> x-peer:nth-of-type(19)) { + --peers-per-row: 7; + } + + x-peers:has(> x-peer:nth-of-type(22)) { + --peers-per-row: 8; + } + + x-peers:has(> x-peer:nth-of-type(25)) { + --peers-per-row: 9; + } + + x-peers:has(> x-peer:nth-of-type(28)) { + --peers-per-row: 10; + } +} + +::-webkit-scrollbar { + display: none; +} + +/* Empty Peers List */ + +x-no-peers { + display: flex; + flex-direction: column; + padding: 8px; + text-align: center; + animation: fade-in 300ms; + animation-fill-mode: backwards; + /* prevent flickering on load */ + animation-iteration-count: 0; +} + +x-no-peers h2, +x-no-peers a { + color: var(--primary-color); + margin-bottom: 5px; +} + +x-peers:not(:empty)+x-no-peers { + display: none; +} + +x-no-peers::before { + color: var(--primary-color); + font-size: 24px; + font-weight: 400; + letter-spacing: -.012em; + line-height: 32px; +} + +x-no-peers[drop-bg]::before { + content: "Release to select recipient"; +} + +x-no-peers[drop-bg] * { + display: none; +} + + + +/* Peer */ + +x-peer { + padding: 8px; + align-content: start; + flex-wrap: wrap; +} + +x-peer label { + width: var(--peer-width); + cursor: pointer; + touch-action: manipulation; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + position: relative; +} + +input[type="file"] { + visibility: hidden; + position: absolute; +} + +x-peer x-icon { + --icon-size: 40px; + margin-bottom: 4px; + transition: transform 150ms; + will-change: transform; + display: flex; + flex-direction: column; +} + +x-peer .icon-wrapper { + width: var(--icon-size); + padding: 12px; + border-radius: 50%; + background: var(--primary-color); + color: white; + display: flex; +} + +x-peer:not(.type-ip).type-secret .icon-wrapper { + background: var(--paired-device-color); +} + +x-peer x-icon > .highlight-wrapper { + align-self: center; + align-items: center; + margin: 7px auto 0; + height: 6px; +} + +x-peer x-icon > .highlight-wrapper > .highlight { + width: 6px; + height: 6px; + border-radius: 50%; + display: none; +} + +x-peer.type-secret x-icon > .highlight-wrapper > .highlight { + background-color: var(--paired-device-color); + display: inline; +} + +x-peer:not([status]):hover x-icon, +x-peer:not([status]):focus x-icon { + transform: scale(1.05); +} + +x-peer[status] x-icon { + box-shadow: none; + opacity: 0.8; + transform: scale(1); +} + +.device-descriptor { + width: 100%; + text-align: center; +} + +.device-descriptor > div { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} + +.status, +.device-name, +.connection-hash { + opacity: 0.7; +} + +.device-name { + font-size: 14px; + white-space: nowrap; +} + +.connection-hash { + font-size: 12px; + white-space: nowrap; +} + +x-peer[status=transfer] .status:before { + content: 'Transferring...'; +} + +x-peer[status=prepare] .status:before { + content: 'Preparing...'; +} + +x-peer[status=wait] .status:before { + content: 'Waiting...'; +} + +x-peer[status=process] .status:before { + content: 'Processing...'; +} + +x-peer:not([status]) .status, +x-peer[status] .device-name { + display: none; +} + +x-peer[status] { + pointer-events: none; +} + +x-peer x-icon { + animation: pop 600ms ease-out 1; +} + +@keyframes pop { + 0% { + transform: scale(0.7); + } + + 40% { + transform: scale(1.2); + } +} + +x-peer[drop] x-icon { + transform: scale(1.1); +} + + + +/* Footer */ + +footer { + position: relative; + margin-top: auto; + z-index: 2; + align-items: center; + padding: 0 0 16px 0; + text-align: center; + transition: color 300ms; + cursor: default; +} + +footer .logo { + --icon-size: 80px; + margin-bottom: 8px; + color: var(--primary-color); + margin-top: -10px; +} + +footer .font-body2 { + color: var(--primary-color); + margin: auto 18px; +} + +#on-this-network { + border-bottom: solid 4px var(--primary-color); + padding-bottom: 1px; +} + +#paired-devices { + border-bottom: solid 4px var(--paired-device-color); + padding-bottom: 1px; +} + +#display-name { + display: inline-block; + text-align: left; + border: none; + outline: none; + max-width: 15em; + text-overflow: ellipsis; + white-space: nowrap; + cursor: text; + margin-left: -1rem; + margin-bottom: -6px; + padding-right: 0.3rem; + padding-left: 0.3em; + padding-bottom: 0.1rem; + border-radius: 1.3rem/30%; + border-right: solid 1rem transparent; + border-left: solid 1rem transparent; + background-clip: padding-box; + background-color: rgba(var(--text-color), 43%); + color: white; + transition: background-color 0.5s ease; + overflow: hidden; +} + +#edit-pen { + width: 1rem; + height: 1rem; + margin-left: -1rem; + margin-bottom: -2px; + position: relative; + z-index: -1; +} + +/* Dialog */ + +x-dialog x-background { + background: rgba(0, 0, 0, 0.61); + z-index: 10; + transition: opacity 300ms; + will-change: opacity; + padding: 15px; + overflow: overlay; +} + +x-dialog x-paper { + z-index: 3; + background: white; + border-radius: 8px; + padding: 16px 24px; + width: 100%; + max-width: 400px; + overflow: hidden; + box-sizing: border-box; + transition: transform 300ms; + will-change: transform; +} + +#pair-device-dialog x-paper { + display: flex; + flex-direction: column; + position: absolute; + top: max(50%, 350px); + margin-top: -328.5px; + width: calc(100vw - 20px); + height: 625px; +} + +#pair-device-dialog ::-moz-selection, +#pair-device-dialog ::selection { + color: black; + background: var(--paired-device-color); +} + +x-dialog:not([show]) { + pointer-events: none; +} + +x-dialog:not([show]) x-paper { + transform: scale(0.1); +} + +x-dialog:not([show]) x-background { + opacity: 0; +} + + +x-dialog a { + color: var(--primary-color); +} + +x-dialog .font-subheading { + margin-bottom: 5px; +} + +/* Pair Devices Dialog */ + +#key-input-container { + width: 100%; + display: flex; + justify-content: center; +} + +#key-input-container > input { + width: 45px; + height: 45px; + font-size: 30px; + padding: 0; + text-align: center; + display: -webkit-box !important; + display: -webkit-flex !important; + display: -moz-flex !important; + display: -ms-flexbox !important; + display: flex !important; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; +} + +#key-input-container > input + * { + margin-left: 6px; +} + +#key-input-container > input:nth-of-type(4) { + margin-left: 5%; +} + +#room-key { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + font-size: 50px; + letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); + display: inline-block; + text-indent: calc(0.5 * (11px + min(calc((100vw - 80px - 99px) / 100 * 6), 23px))); + margin: 15px -15px; +} + +#room-key-qr-code { + margin: 16px; +} + +x-dialog hr { + margin: 40px -24px 30px -24px; + border: solid 1.25px var(--border-color); +} + +#pair-device-dialog x-background { + padding: 16px!important; +} + +/* Edit Paired Devices Dialog */ +.paired-devices-wrapper:empty:before { + content: "No paired devices."; +} + +.paired-devices-wrapper:empty { + padding: 10px; +} + +.paired-devices-wrapper { + border-top: solid 4px var(--paired-device-color); + border-bottom: solid 4px var(--paired-device-color); + max-height: 65vh; + overflow: scroll; + background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)), + linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%, + /* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)), + radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%; + + background-repeat: no-repeat; + background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +.paired-device { + display: flex; + justify-content: space-between; + flex-direction: column; + align-items: center; +} + +.paired-device:not(:last-child) { + border-bottom: solid 4px var(--paired-device-color); +} + +.paired-device > .display-name, +.paired-device > .device-name { + width: 100%; + height: 36px; + display: flex; + align-items: center; + text-align: center; + align-self: center; + border-bottom: solid 2px rgba(128, 128, 128, 0.5); + opacity: 1; +} +.paired-device span { + width: 100%; +} + +.paired-device > .button-wrapper { + display: flex; + height: 36px; + justify-content: space-between; + flex-direction: row; + align-items: center; + width: 100%; +} + +.paired-device > .button-wrapper > label, +.paired-device > .button-wrapper > button { + display: flex; + align-items: center; + text-align: center; + white-space: nowrap; + justify-content: center; + width: 50%; + padding-left: 6px; + padding-right: 6px; + height: 36px; +} + +.paired-device > .button-wrapper > :not(:last-child) { + border-right: solid 1px rgba(128, 128, 128, 0.5); +} + +.paired-device > .button-wrapper > :not(:first-child) { + border-left: solid 1px rgba(128, 128, 128, 0.5); +} + +.paired-device * { + overflow: hidden; + text-overflow: ellipsis; +} + +.paired-device > .auto-accept { + cursor: pointer; +} + +/* Receive Dialog */ + +x-dialog .row { + margin-top: 24px; + margin-bottom: 8px; +} + +/* button row*/ +x-paper > div:last-child { + margin: auto -24px -15px; + border-top: solid 2.5px var(--border-color); + height: 50px; +} + +x-paper > div:last-child > .button { + height: 100%; + width: 100%; +} + +x-paper > div:last-child > .button:not(:last-child) { + border-left: solid 2.5px var(--border-color); +} + +.file-description { + margin-bottom: 25px; +} + +.file-description .row { + margin: 0 +} + +.file-description span { + display: inline; + word-break: normal; +} + +.file-name { + font-style: italic; + max-width: 100%; +} + +.file-stem { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Send Text Dialog */ +/* Todo: add pair underline to send / receive dialogs displayName */ +x-dialog .dialog-subheader { + margin-bottom: 25px; +} + +#text-input { + min-height: 200px; + margin: 14px auto; +} + +/* Receive Text Dialog */ + +#receive-text-dialog #text { + width: 100%; + word-break: break-all; + max-height: calc(100vh - 393px); + overflow-x: hidden; + overflow-y: auto; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + white-space: pre-wrap; + padding: 15px 0; +} + +#receive-text-dialog #text a { + cursor: pointer; +} + +#receive-text-dialog #text a:hover { + text-decoration: underline; +} + +#receive-text-dialog h3 { + /* Select the received text when double-clicking the dialog */ + user-select: none; + pointer-events: none; +} + +.row-separator { + border-bottom: solid 2.5px var(--border-color); + margin: auto -24px; +} + +#base64-paste-btn, +#base64-paste-dialog .textarea { + width: 100%; + height: 40vh; + border: solid 12px #438cff; + text-align: center; +} + +#base64-paste-dialog .textarea { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +#base64-paste-dialog .textarea::before { + font-size: 15px; + letter-spacing: 0.12em; + color: var(--primary-color); + font-weight: 700; + text-transform: uppercase; + content: attr(placeholder); +} + +#base64-paste-dialog button { + margin: auto; + border-radius: 8px; +} + +#base64-paste-dialog button[close] { + margin-top: 20px; +} + +#base64-paste-dialog button[close]:before { + border-radius: 8px; +} + +/* Button */ + +.button { + padding: 2px 16px 0; + box-sizing: border-box; + min-height: 36px; + font-size: 14px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + white-space: nowrap; + cursor: pointer; + user-select: none; + background: inherit; + color: var(--primary-color); + overflow: hidden; +} + +.button[disabled] { + color: #5B5B66; +} + + +.button, +.icon-button { + position: relative; + display: flex; + align-items: center; + justify-content: center; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + touch-action: manipulation; + border: none; + outline: none; +} + +.button:before, +.icon-button:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: currentColor; + opacity: 0; + transition: opacity 300ms; +} + +.button:not([disabled]):hover:before, +.icon-button:hover:before { + opacity: 0.1; +} + +#cancel-paste-mode { + z-index: 2; + margin: 0; + padding: 0; + position: absolute; + top: 0; + right: 0; + left: 0; + width: 100vw; + height: 56px; + background-color: var(--primary-color); + color: rgb(238, 238, 238); +} + +.button:focus:before, +.icon-button:focus:before { + opacity: 0.2; +} + + + +button::-moz-focus-inner { + border: 0; +} + + +/* Icon Button */ +.icon-button { + width: 40px; + height: 40px; +} + +.icon-button:before { + border-radius: 50%; +} + +/* Text Input */ +.textarea { + box-sizing: border-box; + border: none; + outline: none; + padding: 16px 24px; + border-radius: 16px; + margin: 10px 0; + font-size: 14px; + font-family: inherit; + background: #f1f3f4; + display: block; + overflow: auto; + resize: none; + line-height: 16px; + max-height: calc(100vh - 254px); + white-space: pre; +} + + +/* Info Animation */ + +#about { + color: white; + z-index: 11; + overflow: hidden; + pointer-events: none; + text-align: center; +} + +#about .fade-in { + transition: opacity 300ms; + will-change: opacity; + transition-delay: 300ms; + z-index: 11; + pointer-events: all; +} + +#about:not(:target) .fade-in { + opacity: 0; + pointer-events: none; + transition-delay: 0s; +} + +#about .logo { + --icon-size: 96px; +} + +#about .title-wrapper { + display: flex; + align-items: baseline; +} + +#about .title-wrapper > div { + margin-left: 0.5em; +} + +#about x-background { + position: absolute; + --size: max(max(230vw, 230vh), calc(150vh + 150vw)); + --size-half: calc(var(--size)/2); + top: calc(28px - var(--size-half)); + right: calc(36px - var(--size-half)); + width: var(--size); + height: var(--size); + border-radius: 50%; + background: var(--primary-color); + transform: scale(0); + z-index: -1; +} + +/* Hack such that initial scale(0) isn't animated */ +#about x-background { + will-change: transform; + transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1); +} + +#about:target x-background { + transform: scale(1); +} + +#about .row a { + margin: 8px 8px -16px; +} + +#about section { + flex-grow: 1; +} + +#about header { + align-self: end; +} + +canvas.circles { + width: 100vw; + position: absolute; + z-index: -10; + top: 0; + left: 0; +} + +/* Loading Indicator */ + +.progress { + width: 80px; + height: 80px; + position: absolute; + top: -8px; + clip: rect(0px, 80px, 80px, 40px); + --progress: rotate(0deg); + transition: transform 200ms; +} + +.circle { + width: 72px; + height: 72px; + border: 4px solid var(--primary-color); + border-radius: 40px; + position: absolute; + clip: rect(0px, 40px, 80px, 0px); + will-change: transform; + transform: var(--progress); +} + +.over50 { + clip: rect(auto, auto, auto, auto); +} + +.over50 .circle.right { + transform: rotate(180deg); +} + + +/* Generic placeholder */ +[placeholder]:empty:before { + content: attr(placeholder); +} + +/* Toast */ + +.toast-container { + padding: 0 8px 24px; + overflow: hidden; + pointer-events: none; +} + +x-toast { + position: absolute; + min-height: 48px; + top: 50px; + width: 100%; + max-width: 344px; + background-color: rgb(var(--text-color)); + color: rgb(var(--bg-color)); + align-items: center; + box-sizing: border-box; + padding: 8px 24px; + z-index: 20; + transition: opacity 200ms, transform 300ms ease-out; + cursor: default; + line-height: 24px; + border-radius: 8px; + pointer-events: all; +} + +x-toast:not([show]):not(:hover) { + opacity: 0; + transform: translateY(-100px); +} + + +/* Instructions */ + +x-instructions { + position: relative; + opacity: 0.5; + transition: opacity 300ms; + text-align: center; + margin-left: 10px; + margin-right: 10px; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; +} + +x-instructions:not([drop-peer]):not([drop-bg]):before { + content: attr(mobile); +} + +x-instructions[drop-peer]:before { + content: "Release to send to peer"; +} + +x-instructions[drop-bg]:not([drop-peer]):before { + content: "Release to select recipient"; +} + +x-instructions p { + display: none; +} + +x-peers:empty~x-instructions { + opacity: 0; +} + +@media (hover: none) and (pointer: coarse) { + x-peer { + transform: scale(0.95); + padding: 4px; + } +} + +/* Responsive Styles */ +@media screen and (max-width: 360px) { + x-dialog x-paper { + padding: 15px; + } + x-paper > div:last-child { + margin: auto -15px -15px; + } +} + +@media screen and (min-height: 800px) { + footer { + margin-bottom: 16px; + } +} + +@media (hover: hover) and (pointer: fine) { + x-instructions:not([drop-peer]):not([drop-bg]):before { + content: attr(desktop); + } +} + +/* + Color Themes +*/ + +/* Default colors */ +body { + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ + --bg-color-test: 18,18,18; + --bg-color-secondary: #f1f3f4; + --border-color: #e7e8e8; +} + +/* Dark theme colors */ +body.dark-theme { + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ + --bg-color-secondary: #333; + --border-color: #252525; +} + +/* Colored Elements */ +body { + color: rgb(var(--text-color)); + background-color: rgb(var(--bg-color)); + transition: background-color 0.5s ease; +} + +x-dialog x-paper { + background-color: rgb(var(--bg-color)); +} + +.textarea { + color: rgb(var(--text-color)) !important; + background-color: var(--bg-color-secondary) !important; +} + +.textarea * { + margin: 0 !important; + padding: 0 !important; + color: unset !important; + background: unset !important; + border: unset !important; + opacity: unset !important; + font-family: inherit !important; + font-size: inherit !important; + font-style: unset !important; + font-weight: unset !important; +} + +/* Image/Video/Audio Preview */ +.file-preview { + margin: 10px -24px 40px -24px; +} + +.file-preview:empty { + display: none; +} + +.file-preview > img, +.file-preview > audio, +.file-preview > video { + max-width: 100%; + max-height: 40vh; + margin: auto; + display: block; +} + +/* Styles for users who prefer dark mode at the OS level */ +@media (prefers-color-scheme: dark) { + + /* defaults to dark theme */ + body { + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ + --bg-color-secondary: #333; + --border-color: #252525; + } + + /* Override dark mode with light mode styles if the user decides to swap */ + body.light-theme { + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ + --bg-color-secondary: #f1f3f4; + --border-color: #e7e8e8; + } +} + + +/* + Edge specific styles +*/ +@supports (-ms-ime-align: auto) { + + html, + body { + overflow: hidden; + } +} + +/* + iOS specific styles +*/ +@supports (-webkit-overflow-scrolling: touch) { + html { + min-height: -webkit-fill-available; + } +} + +/* webkit scrollbar style*/ + +::-webkit-scrollbar{ + width: 4px; + height: 4px; +} + +::-webkit-scrollbar-thumb{ + background: #bfbfbf; + border-radius: 4px; +} + +::-moz-selection, +::selection { + color: black; + background: var(--primary-color); +} diff --git a/public_included_ws_fallback/images/android-chrome-192x192-maskable.png b/public_included_ws_fallback/images/android-chrome-192x192-maskable.png new file mode 100644 index 0000000..f0e9245 Binary files /dev/null and b/public_included_ws_fallback/images/android-chrome-192x192-maskable.png differ diff --git a/public_included_ws_fallback/images/android-chrome-192x192.png b/public_included_ws_fallback/images/android-chrome-192x192.png new file mode 100644 index 0000000..0bdca51 Binary files /dev/null and b/public_included_ws_fallback/images/android-chrome-192x192.png differ diff --git a/public_included_ws_fallback/images/android-chrome-512x512-maskable.png b/public_included_ws_fallback/images/android-chrome-512x512-maskable.png new file mode 100644 index 0000000..cdda606 Binary files /dev/null and b/public_included_ws_fallback/images/android-chrome-512x512-maskable.png differ diff --git a/public_included_ws_fallback/images/android-chrome-512x512.png b/public_included_ws_fallback/images/android-chrome-512x512.png new file mode 100644 index 0000000..b01e679 Binary files /dev/null and b/public_included_ws_fallback/images/android-chrome-512x512.png differ diff --git a/public_included_ws_fallback/images/apple-touch-icon.png b/public_included_ws_fallback/images/apple-touch-icon.png new file mode 100644 index 0000000..0a32878 Binary files /dev/null and b/public_included_ws_fallback/images/apple-touch-icon.png differ diff --git a/public_included_ws_fallback/images/favicon-96x96-notification.png b/public_included_ws_fallback/images/favicon-96x96-notification.png new file mode 100644 index 0000000..d3407c3 Binary files /dev/null and b/public_included_ws_fallback/images/favicon-96x96-notification.png differ diff --git a/public_included_ws_fallback/images/favicon-96x96.png b/public_included_ws_fallback/images/favicon-96x96.png new file mode 100644 index 0000000..b8a5746 Binary files /dev/null and b/public_included_ws_fallback/images/favicon-96x96.png differ diff --git a/public_included_ws_fallback/images/logo_blue_512x512.png b/public_included_ws_fallback/images/logo_blue_512x512.png new file mode 100644 index 0000000..41d13fc Binary files /dev/null and b/public_included_ws_fallback/images/logo_blue_512x512.png differ diff --git a/public_included_ws_fallback/images/logo_transparent_128x128.png b/public_included_ws_fallback/images/logo_transparent_128x128.png new file mode 100644 index 0000000..c276efe Binary files /dev/null and b/public_included_ws_fallback/images/logo_transparent_128x128.png differ diff --git a/public_included_ws_fallback/images/logo_transparent_512x512.png b/public_included_ws_fallback/images/logo_transparent_512x512.png new file mode 100644 index 0000000..367e24f Binary files /dev/null and b/public_included_ws_fallback/images/logo_transparent_512x512.png differ diff --git a/public_included_ws_fallback/images/logo_transparent_white_512x512.png b/public_included_ws_fallback/images/logo_transparent_white_512x512.png new file mode 100644 index 0000000..37589b6 Binary files /dev/null and b/public_included_ws_fallback/images/logo_transparent_white_512x512.png differ diff --git a/public_included_ws_fallback/images/logo_white_512x512.png b/public_included_ws_fallback/images/logo_white_512x512.png new file mode 100644 index 0000000..d7750d9 Binary files /dev/null and b/public_included_ws_fallback/images/logo_white_512x512.png differ diff --git a/public_included_ws_fallback/images/moreload_screenshot_mobile_1.png b/public_included_ws_fallback/images/moreload_screenshot_mobile_1.png new file mode 100644 index 0000000..d93aafb Binary files /dev/null and b/public_included_ws_fallback/images/moreload_screenshot_mobile_1.png differ diff --git a/public_included_ws_fallback/images/moreload_screenshot_mobile_2.png b/public_included_ws_fallback/images/moreload_screenshot_mobile_2.png new file mode 100644 index 0000000..51ace10 Binary files /dev/null and b/public_included_ws_fallback/images/moreload_screenshot_mobile_2.png differ diff --git a/public_included_ws_fallback/images/moreload_screenshot_mobile_3.png b/public_included_ws_fallback/images/moreload_screenshot_mobile_3.png new file mode 100644 index 0000000..57ad15a Binary files /dev/null and b/public_included_ws_fallback/images/moreload_screenshot_mobile_3.png differ diff --git a/public_included_ws_fallback/images/moreload_screenshot_mobile_4.png b/public_included_ws_fallback/images/moreload_screenshot_mobile_4.png new file mode 100644 index 0000000..d5811ad Binary files /dev/null and b/public_included_ws_fallback/images/moreload_screenshot_mobile_4.png differ diff --git a/public_included_ws_fallback/images/moreload_screenshot_mobile_5.png b/public_included_ws_fallback/images/moreload_screenshot_mobile_5.png new file mode 100644 index 0000000..d205fd9 Binary files /dev/null and b/public_included_ws_fallback/images/moreload_screenshot_mobile_5.png differ diff --git a/public_included_ws_fallback/images/moreload_screenshot_mobile_6.png b/public_included_ws_fallback/images/moreload_screenshot_mobile_6.png new file mode 100644 index 0000000..23c06ae Binary files /dev/null and b/public_included_ws_fallback/images/moreload_screenshot_mobile_6.png differ diff --git a/public_included_ws_fallback/images/moreload_screenshot_mobile_7.png b/public_included_ws_fallback/images/moreload_screenshot_mobile_7.png new file mode 100644 index 0000000..c32980a Binary files /dev/null and b/public_included_ws_fallback/images/moreload_screenshot_mobile_7.png differ diff --git a/public_included_ws_fallback/images/mstile-150x150.png b/public_included_ws_fallback/images/mstile-150x150.png new file mode 100644 index 0000000..6380e32 Binary files /dev/null and b/public_included_ws_fallback/images/mstile-150x150.png differ diff --git a/public_included_ws_fallback/images/safari-pinned-tab.svg b/public_included_ws_fallback/images/safari-pinned-tab.svg new file mode 100644 index 0000000..263ee4e --- /dev/null +++ b/public_included_ws_fallback/images/safari-pinned-tab.svg @@ -0,0 +1,251 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public_included_ws_fallback/images/snapdrop-graphics.sketch b/public_included_ws_fallback/images/snapdrop-graphics.sketch new file mode 100644 index 0000000..b8b756a Binary files /dev/null and b/public_included_ws_fallback/images/snapdrop-graphics.sketch differ diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html new file mode 100644 index 0000000..9b58b04 --- /dev/null +++ b/public_included_ws_fallback/index.html @@ -0,0 +1,392 @@ + + + + + + + + MoreLoad + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+
+ + + + + +
+ +
+ +
+ + +

Open MoreLoad on other devices to send files

+
Pair devices to be discoverable on other networks
+
+ +

+
+
+ +
+ +
+ You are known as: +
+ + + +
+
+ You can be discovered by everyone on this network + +
+
+ Traffic is routed through the server if WebRTC is not available. +
+
+ + +
+ + +

Pair Devices

+
+

000 000

+
Input this key on another device
or scan the QR-Code.
+
+
+ + + + + + +
+
Enter key from another device to continue.
+
+ + +
+
+
+
+
+ + +
+ + +

Edit Paired Devices

+
+
+

Activate auto-accept to automatically accept all files sent from that device.

+
+
+ +
+
+
+
+
+ + + + +

+
+
+ + would like to share +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+ + + + +

+
+
+ + has sent +
+
+ + +
+
+
+
+
+
+ + + +
+
+
+
+ + +
+ + +

Send Message

+
+ Send a Message to + +
+
+
+
+ + +
+
+
+
+
+ + + + +

Message Received

+
+ + has sent: +
+
+
+
+ + +
+
+
+
+ + + + + + + + + + + +
+ +
+ + +
+ + + + + +
+
+ +
+

MoreLoad

+
v1.7.3
+
+
The easiest way to transfer files across devices
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public_included_ws_fallback/manifest.json b/public_included_ws_fallback/manifest.json new file mode 100644 index 0000000..b99b067 --- /dev/null +++ b/public_included_ws_fallback/manifest.json @@ -0,0 +1,281 @@ +{ + "name": "MoreLoad", + "short_name": "MoreLoad", + "icons": [{ + "src": "images/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + },{ + "src": "images/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + },{ + "src": "images/android-chrome-192x192-maskable.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + },{ + "src": "images/android-chrome-512x512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + },{ + "src": "images/favicon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }], + "background_color": "#efefef", + "start_url": "/", + "scope": "/", + "display": "minimal-ui", + "theme_color": "#3367d6", + "screenshots" : [ + { + "src": "images/moreload_screenshot_mobile_1.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_2.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_3.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_4.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_5.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_6.png", + "sizes": "1170x2532", + "type": "image/png" + }, + { + "src": "images/moreload_screenshot_mobile_7.png", + "sizes": "1170x2532", + "type": "image/png" + } + ], + "share_target": { + "action": "/", + "method":"POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [{ + "name": "allfiles", + "accept": ["*/*"] + }] + } + }, + "file_handlers": [ + { + "action": "/?file_handler", + "name": "All Files", + "accept": { + "application/cpl+xml": [".cpl"], + "application/gpx+xml": [".gpx"], + "application/gzip": [".gz"], + "application/java-archive": [".jar", ".war", ".ear"], + "application/java-vm": [".class"], + "application/javascript": [".js", ".mjs"], + "application/json": [".json", ".map"], + "application/manifest+json": [".webmanifest"], + "application/msword": [".doc", ".dot", ".wiz"], + "application/octet-stream": [".bin", ".dms", ".lrf", ".mar", ".so", ".dist", ".distz", ".pkg", ".bpk", ".dump", ".elc", ".deploy", ".exe", ".dll", ".deb", ".dmg", ".iso", ".img", ".msi", ".msp", ".msm", ".buffer"], + "application/oda": [".oda"], + "application/oxps": [".oxps"], + "application/pdf": [".pdf"], + "application/pgp-signature": [".asc", ".sig"], + "application/pics-rules": [".prf"], + "application/pkcs7-mime": [".p7c"], + "application/pkix-cert": [".cer"], + "application/postscript": [".ai", ".eps", ".ps"], + "application/rtf": [".rtf"], + "application/vnd.android.package-archive": [".apk"], + "application/vnd.apple.mpegurl": [".m3u", ".m3u8"], + "application/vnd.apple.pkpass": [".pkpass"], + "application/vnd.google-earth.kml+xml": [".kml"], + "application/vnd.google-earth.kmz": [".kmz"], + "application/vnd.ms-cab-compressed": [".cab"], + "application/vnd.ms-excel": [".xls", ".xlm", ".xla", ".xlc", ".xlt", ".xlw"], + "application/vnd.ms-outlook": [".msg"], + "application/vnd.ms-powerpoint": [".ppt", ".pot", ".ppa", ".pps", ".pwz"], + "application/vnd.ms-project": [".mpp", ".mpt"], + "application/vnd.ms-xpsdocument": [".xps"], + "application/vnd.oasis.opendocument.database": [".odb"], + "application/vnd.oasis.opendocument.spreadsheet": [".ods"], + "application/vnd.oasis.opendocument.text": [".odt"], + "application/vnd.openstreetmap.data+xml": [".osm"], + "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"], + "application/vnd.tcpdump.pcap": [".pcap", ".cap", ".dmp"], + "application/vnd.wordperfect": [".wpd"], + "application/wasm": [".wasm"], + "application/x-7z-compressed": [".7z"], + "application/x-apple-diskimage": [".dmg"], + "application/x-bcpio": [".bcpio"], + "application/x-bittorrent": [".torrent"], + "application/x-cbr": [".cbr", ".cba", ".cbt", ".cbz", ".cb7"], + "application/x-cdlink": [".vcd"], + "application/x-chrome-extension": [".crx"], + "application/x-cpio": [".cpio"], + "application/x-csh": [".csh"], + "application/x-debian-package": [".deb", ".udeb"], + "application/x-dvi": [".dvi"], + "application/x-freearc": [".arc"], + "application/x-gtar": [".gtar"], + "application/x-hdf": [".hdf"], + "application/x-hdf5": [".h5"], + "application/x-httpd-php": [".php"], + "application/x-iso9660-image": [".iso"], + "application/x-iwork-keynote-sffkey": [".key"], + "application/x-iwork-numbers-sffnumbers": [".numbers"], + "application/x-iwork-pages-sffpages": [".pages"], + "application/x-latex": [".latex"], + "application/x-makeself": [".run"], + "application/x-mif": [".mif"], + "application/x-ms-shortcut": [".lnk"], + "application/x-msaccess": [".mdb"], + "application/x-msdownload": [".exe", ".dll", ".com", ".bat", ".msi"], + "application/x-mspublisher": [".pub"], + "application/x-netcdf": [".cdf", ".nc"], + "application/x-perl": [".pl", ".pm"], + "application/x-pilot": [".prc", ".pdb"], + "application/x-pkcs12": [".p12", ".pfx"], + "application/x-pn-realaudio": [".ram"], + "application/x-python-code": [".pyc", ".pyo"], + "application/x-rar-compressed": [".rar"], + "application/x-redhat-package-manager": [".rpm"], + "application/x-sh": [".sh"], + "application/x-shar": [".shar"], + "application/x-shockwave-flash": [".swf"], + "application/x-sql": [".sql"], + "application/x-subrip": [".srt"], + "application/x-sv4cpio": [".sv4cpio"], + "application/x-sv4crc": [".sv4crc"], + "application/x-tads": [".gam"], + "application/x-tar": [".tar"], + "application/x-tcl": [".tcl"], + "application/x-tex": [".tex"], + "application/x-troff": [".roff", ".t", ".tr"], + "application/x-troff-man": [".man"], + "application/x-troff-me": [".me"], + "application/x-troff-ms": [".ms"], + "application/x-ustar": [".ustar"], + "application/x-wais-source": [".src"], + "application/x-xpinstall": [".xpi"], + "application/xhtml+xml": [".xhtml", ".xht"], + "application/xml": [".xsl", ".rdf", ".wsdl", ".xpdl"], + "application/zip": [".zip"], + "audio/3gpp": [".3gp", ".3gpp"], + "audio/3gpp2": [".3g2", ".3gpp2"], + "audio/aac": [".aac", ".adts", ".loas", ".ass"], + "audio/basic": [".au", ".snd"], + "audio/midi": [".mid", ".midi", ".kar", ".rmi"], + "audio/mpeg": [".mpga", ".mp2", ".mp2a", ".mp3", ".m2a", ".m3a"], + "audio/ogg": [".oga", ".ogg", ".spx", ".opus"], + "audio/opus": [".opus"], + "audio/x-aiff": [".aif", ".aifc", ".aiff"], + "audio/x-flac": [".flac"], + "audio/x-m4a": [".m4a"], + "audio/x-mpegurl": [".m3u"], + "audio/x-ms-wma": [".wma"], + "audio/x-pn-realaudio": [".ra"], + "audio/x-wav": [".wav"], + "font/otf": [".otf"], + "font/ttf": [".ttf"], + "font/woff": [".woff"], + "font/woff2": [".woff2"], + "image/emf": [".emf"], + "image/gif": [".gif"], + "image/heic": [".heic"], + "image/heif": [".heif"], + "image/ief": [".ief"], + "image/jpeg": [".jpeg", ".jpg"], + "image/jpg": [".jpg"], + "image/pict": [".pict", ".pct", ".pic"], + "image/png": [".png"], + "image/svg+xml": [".svg", ".svgz"], + "image/tiff": [".tif", ".tiff"], + "image/vnd.adobe.photoshop": [".psd"], + "image/vnd.djvu": [".djvu", ".djv"], + "image/vnd.dwg": [".dwg"], + "image/vnd.dxf": [".dxf"], + "image/vnd.microsoft.icon": [".ico"], + "image/vnd.ms-dds": [".dds"], + "image/x-3ds": [".3ds"], + "image/x-cmu-raster": [".ras"], + "image/x-icon": [".ico"], + "image/x-ms-bmp": [".bmp"], + "image/x-portable-anymap": [".pnm"], + "image/x-portable-bitmap": [".pbm"], + "image/x-portable-graymap": [".pgm"], + "image/x-portable-pixmap": [".ppm"], + "image/x-rgb": [".rgb"], + "image/x-tga": [".tga"], + "image/x-xbitmap": [".xbm"], + "image/x-xpixmap": [".xpm"], + "image/x-xwindowdump": [".xwd"], + "message/rfc822": [".eml", ".mht", ".mhtml", ".nws"], + "model/obj": [".obj"], + "model/stl": [".stl"], + "model/vnd.collada+xml": [".dae"], + "text/calendar": [".ics", ".ifb"], + "text/css": [".css"], + "text/csv": [".csv"], + "text/html": [".html", ".htm", ".shtml"], + "text/markdown": [".markdown", ".md"], + "text/plain": [".txt", ".text", ".conf", ".def", ".list", ".log", ".in", ".ini"], + "text/richtext": [".rtx"], + "text/rtf": [".rtf"], + "text/tab-separated-values": [".tsv"], + "text/x-c": [".c", ".cc", ".cxx", ".cpp", ".h", ".hh", ".dic"], + "text/x-java-source": [".java"], + "text/x-lua": [".lua"], + "text/x-python": [".py"], + "text/x-setext": [".etx"], + "text/x-sgml": [".sgm", ".sgml"], + "text/x-vcard": [".vcf"], + "text/xml": [".xml"], + "text/xul": [".xul"], + "text/yaml": [".yaml", ".yml"], + "video/3gpp": [".3gp", ".3gpp"], + "video/mp2t": [".ts"], + "video/mp4": [".mp4", ".mp4v", ".mpg4"], + "video/mpeg": [".mpeg", ".m1v", ".mpa", ".mpe", ".mpg"], + "video/quicktime": [".mov", ".qt"], + "video/webm": [".webm"], + "video/x-flv": [".flv"], + "video/x-m4v": [".m4v"], + "video/x-ms-asf": [".asf", ".asx"], + "video/x-ms-vob": [".vob"], + "video/x-ms-wmv": [".wmv"], + "video/x-msvideo": [".avi"], + "video/x-sgi-movie": [".*"] + }, + "icons": [ + { + "src": "/images/android-chrome-192x192.png", + "sizes": "192x192" + } + ] + } + ], + "launch_handler": { + "client_mode": "focus-existing" + } +} diff --git a/public_included_ws_fallback/robots.txt b/public_included_ws_fallback/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public_included_ws_fallback/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public_included_ws_fallback/scripts/NoSleep.min.js b/public_included_ws_fallback/scripts/NoSleep.min.js new file mode 100644 index 0000000..202c35c --- /dev/null +++ b/public_included_ws_fallback/scripts/NoSleep.min.js @@ -0,0 +1,2 @@ +/*! NoSleep.min.js v0.12.0 - git.io/vfn01 - Rich Tibbett - MIT license */ +!function(A,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.NoSleep=e():A.NoSleep=e()}(this,(function(){return function(A){var e={};function B(g){if(e[g])return e[g].exports;var o=e[g]={i:g,l:!1,exports:{}};return A[g].call(o.exports,o,o.exports,B),o.l=!0,o.exports}return B.m=A,B.c=e,B.d=function(A,e,g){B.o(A,e)||Object.defineProperty(A,e,{enumerable:!0,get:g})},B.r=function(A){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(A,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(A,"__esModule",{value:!0})},B.t=function(A,e){if(1&e&&(A=B(A)),8&e)return A;if(4&e&&"object"==typeof A&&A&&A.__esModule)return A;var g=Object.create(null);if(B.r(g),Object.defineProperty(g,"default",{enumerable:!0,value:A}),2&e&&"string"!=typeof A)for(var o in A)B.d(g,o,function(e){return A[e]}.bind(null,o));return g},B.n=function(A){var e=A&&A.__esModule?function(){return A.default}:function(){return A};return B.d(e,"a",e),e},B.o=function(A,e){return Object.prototype.hasOwnProperty.call(A,e)},B.p="",B(B.s=0)}([function(A,e,B){"use strict";var g=function(){function A(A,e){for(var B=0;B.5&&(e.noSleepVideo.currentTime=Math.random())}))})))}return g(A,[{key:"_addSourceToVideo",value:function(A,e,B){var g=document.createElement("source");g.src=B,g.type="video/"+e,A.appendChild(g)}},{key:"enable",value:function(){var A=this;return Q()?navigator.wakeLock.request("screen").then((function(e){A._wakeLock=e,A.enabled=!0,console.log("Wake Lock active."),A._wakeLock.addEventListener("release",(function(){console.log("Wake Lock released.")}))})).catch((function(e){throw A.enabled=!1,console.error(e.name+", "+e.message),e})):C()?(this.disable(),console.warn("\n NoSleep enabled for older iOS devices. This can interrupt\n active or long-running network requests from completing successfully.\n See https://github.com/richtr/NoSleep.js/issues/15 for more details.\n "),this.noSleepTimer=window.setInterval((function(){document.hidden||(window.location.href=window.location.href.split("#")[0],window.setTimeout(window.stop,0))}),15e3),this.enabled=!0,Promise.resolve()):this.noSleepVideo.play().then((function(e){return A.enabled=!0,e})).catch((function(e){throw A.enabled=!1,e}))}},{key:"disable",value:function(){Q()?(this._wakeLock&&this._wakeLock.release(),this._wakeLock=null):C()?this.noSleepTimer&&(console.warn("\n NoSleep now disabled for older iOS devices.\n "),window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):this.noSleepVideo.pause(),this.enabled=!1}},{key:"isEnabled",get:function(){return this.enabled}}]),A}();A.exports=i},function(A,e,B){"use strict";A.exports={webm:"data:video/webm;base64,GkXfowEAAAAAAAAfQoaBAUL3gQFC8oEEQvOBCEKChHdlYm1Ch4EEQoWBAhhTgGcBAAAAAAAVkhFNm3RALE27i1OrhBVJqWZTrIHfTbuMU6uEFlSua1OsggEwTbuMU6uEHFO7a1OsghV17AEAAAAAAACkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmAQAAAAAAAEUq17GDD0JATYCNTGF2ZjU1LjMzLjEwMFdBjUxhdmY1NS4zMy4xMDBzpJBlrrXf3DCDVB8KcgbMpcr+RImIQJBgAAAAAAAWVK5rAQAAAAAAD++uAQAAAAAAADLXgQFzxYEBnIEAIrWcg3VuZIaFVl9WUDiDgQEj44OEAmJaAOABAAAAAAAABrCBsLqBkK4BAAAAAAAPq9eBAnPFgQKcgQAitZyDdW5khohBX1ZPUkJJU4OBAuEBAAAAAAAAEZ+BArWIQOdwAAAAAABiZIEgY6JPbwIeVgF2b3JiaXMAAAAAAoC7AAAAAAAAgLUBAAAAAAC4AQN2b3JiaXMtAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAxMDExMDEgKFNjaGF1ZmVudWdnZXQpAQAAABUAAABlbmNvZGVyPUxhdmM1NS41Mi4xMDIBBXZvcmJpcyVCQ1YBAEAAACRzGCpGpXMWhBAaQlAZ4xxCzmvsGUJMEYIcMkxbyyVzkCGkoEKIWyiB0JBVAABAAACHQXgUhIpBCCGEJT1YkoMnPQghhIg5eBSEaUEIIYQQQgghhBBCCCGERTlokoMnQQgdhOMwOAyD5Tj4HIRFOVgQgydB6CCED0K4moOsOQghhCQ1SFCDBjnoHITCLCiKgsQwuBaEBDUojILkMMjUgwtCiJqDSTX4GoRnQXgWhGlBCCGEJEFIkIMGQcgYhEZBWJKDBjm4FITLQagahCo5CB+EIDRkFQCQAACgoiiKoigKEBqyCgDIAAAQQFEUx3EcyZEcybEcCwgNWQUAAAEACAAAoEiKpEiO5EiSJFmSJVmSJVmS5omqLMuyLMuyLMsyEBqyCgBIAABQUQxFcRQHCA1ZBQBkAAAIoDiKpViKpWiK54iOCISGrAIAgAAABAAAEDRDUzxHlETPVFXXtm3btm3btm3btm3btm1blmUZCA1ZBQBAAAAQ0mlmqQaIMAMZBkJDVgEACAAAgBGKMMSA0JBVAABAAACAGEoOogmtOd+c46BZDppKsTkdnEi1eZKbirk555xzzsnmnDHOOeecopxZDJoJrTnnnMSgWQqaCa0555wnsXnQmiqtOeeccc7pYJwRxjnnnCateZCajbU555wFrWmOmkuxOeecSLl5UptLtTnnnHPOOeecc84555zqxekcnBPOOeecqL25lpvQxTnnnE/G6d6cEM4555xzzjnnnHPOOeecIDRkFQAABABAEIaNYdwpCNLnaCBGEWIaMulB9+gwCRqDnELq0ehopJQ6CCWVcVJKJwgNWQUAAAIAQAghhRRSSCGFFFJIIYUUYoghhhhyyimnoIJKKqmooowyyyyzzDLLLLPMOuyssw47DDHEEEMrrcRSU2011lhr7jnnmoO0VlprrbVSSimllFIKQkNWAQAgAAAEQgYZZJBRSCGFFGKIKaeccgoqqIDQkFUAACAAgAAAAABP8hzRER3RER3RER3RER3R8RzPESVREiVREi3TMjXTU0VVdWXXlnVZt31b2IVd933d933d+HVhWJZlWZZlWZZlWZZlWZZlWZYgNGQVAAACAAAghBBCSCGFFFJIKcYYc8w56CSUEAgNWQUAAAIACAAAAHAUR3EcyZEcSbIkS9IkzdIsT/M0TxM9URRF0zRV0RVdUTdtUTZl0zVdUzZdVVZtV5ZtW7Z125dl2/d93/d93/d93/d93/d9XQdCQ1YBABIAADqSIymSIimS4ziOJElAaMgqAEAGAEAAAIriKI7jOJIkSZIlaZJneZaomZrpmZ4qqkBoyCoAABAAQAAAAAAAAIqmeIqpeIqoeI7oiJJomZaoqZoryqbsuq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq4LhIasAgAkAAB0JEdyJEdSJEVSJEdygNCQVQCADACAAAAcwzEkRXIsy9I0T/M0TxM90RM901NFV3SB0JBVAAAgAIAAAAAAAAAMybAUy9EcTRIl1VItVVMt1VJF1VNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVN0zRNEwgNWQkAkAEAkBBTLS3GmgmLJGLSaqugYwxS7KWxSCpntbfKMYUYtV4ah5RREHupJGOKQcwtpNApJq3WVEKFFKSYYyoVUg5SIDRkhQAQmgHgcBxAsixAsiwAAAAAAAAAkDQN0DwPsDQPAAAAAAAAACRNAyxPAzTPAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAA0DwP8DwR8EQRAAAAAAAAACzPAzTRAzxRBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAAsDwP8EQR0DwRAAAAAAAAACzPAzxRBDzRAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEOAAABBgIRQasiIAiBMAcEgSJAmSBM0DSJYFTYOmwTQBkmVB06BpME0AAAAAAAAAAAAAJE2DpkHTIIoASdOgadA0iCIAAAAAAAAAAAAAkqZB06BpEEWApGnQNGgaRBEAAAAAAAAAAAAAzzQhihBFmCbAM02IIkQRpgkAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAGHAAAAgwoQwUGrIiAIgTAHA4imUBAIDjOJYFAACO41gWAABYliWKAABgWZooAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAYcAAACDChDBQashIAiAIAcCiKZQHHsSzgOJYFJMmyAJYF0DyApgFEEQAIAAAocAAACLBBU2JxgEJDVgIAUQAABsWxLE0TRZKkaZoniiRJ0zxPFGma53meacLzPM80IYqiaJoQRVE0TZimaaoqME1VFQAAUOAAABBgg6bE4gCFhqwEAEICAByKYlma5nmeJ4qmqZokSdM8TxRF0TRNU1VJkqZ5niiKommapqqyLE3zPFEURdNUVVWFpnmeKIqiaaqq6sLzPE8URdE0VdV14XmeJ4qiaJqq6roQRVE0TdNUTVV1XSCKpmmaqqqqrgtETxRNU1Vd13WB54miaaqqq7ouEE3TVFVVdV1ZBpimaaqq68oyQFVV1XVdV5YBqqqqruu6sgxQVdd1XVmWZQCu67qyLMsCAAAOHAAAAoygk4wqi7DRhAsPQKEhKwKAKAAAwBimFFPKMCYhpBAaxiSEFEImJaXSUqogpFJSKRWEVEoqJaOUUmopVRBSKamUCkIqJZVSAADYgQMA2IGFUGjISgAgDwCAMEYpxhhzTiKkFGPOOScRUoox55yTSjHmnHPOSSkZc8w556SUzjnnnHNSSuacc845KaVzzjnnnJRSSuecc05KKSWEzkEnpZTSOeecEwAAVOAAABBgo8jmBCNBhYasBABSAQAMjmNZmuZ5omialiRpmud5niiapiZJmuZ5nieKqsnzPE8URdE0VZXneZ4oiqJpqirXFUXTNE1VVV2yLIqmaZqq6rowTdNUVdd1XZimaaqq67oubFtVVdV1ZRm2raqq6rqyDFzXdWXZloEsu67s2rIAAPAEBwCgAhtWRzgpGgssNGQlAJABAEAYg5BCCCFlEEIKIYSUUggJAAAYcAAACDChDBQashIASAUAAIyx1lprrbXWQGettdZaa62AzFprrbXWWmuttdZaa6211lJrrbXWWmuttdZaa6211lprrbXWWmuttdZaa6211lprrbXWWmuttdZaa6211lprrbXWWmstpZRSSimllFJKKaWUUkoppZRSSgUA+lU4APg/2LA6wknRWGChISsBgHAAAMAYpRhzDEIppVQIMeacdFRai7FCiDHnJKTUWmzFc85BKCGV1mIsnnMOQikpxVZjUSmEUlJKLbZYi0qho5JSSq3VWIwxqaTWWoutxmKMSSm01FqLMRYjbE2ptdhqq7EYY2sqLbQYY4zFCF9kbC2m2moNxggjWywt1VprMMYY3VuLpbaaizE++NpSLDHWXAAAd4MDAESCjTOsJJ0VjgYXGrISAAgJACAQUooxxhhzzjnnpFKMOeaccw5CCKFUijHGnHMOQgghlIwx5pxzEEIIIYRSSsaccxBCCCGEkFLqnHMQQgghhBBKKZ1zDkIIIYQQQimlgxBCCCGEEEoopaQUQgghhBBCCKmklEIIIYRSQighlZRSCCGEEEIpJaSUUgohhFJCCKGElFJKKYUQQgillJJSSimlEkoJJYQSUikppRRKCCGUUkpKKaVUSgmhhBJKKSWllFJKIYQQSikFAAAcOAAABBhBJxlVFmGjCRcegEJDVgIAZAAAkKKUUiktRYIipRikGEtGFXNQWoqocgxSzalSziDmJJaIMYSUk1Qy5hRCDELqHHVMKQYtlRhCxhik2HJLoXMOAAAAQQCAgJAAAAMEBTMAwOAA4XMQdAIERxsAgCBEZohEw0JweFAJEBFTAUBigkIuAFRYXKRdXECXAS7o4q4DIQQhCEEsDqCABByccMMTb3jCDU7QKSp1IAAAAAAADADwAACQXAAREdHMYWRobHB0eHyAhIiMkAgAAAAAABcAfAAAJCVAREQ0cxgZGhscHR4fICEiIyQBAIAAAgAAAAAggAAEBAQAAAAAAAIAAAAEBB9DtnUBAAAAAAAEPueBAKOFggAAgACjzoEAA4BwBwCdASqwAJAAAEcIhYWIhYSIAgIABhwJ7kPfbJyHvtk5D32ych77ZOQ99snIe+2TkPfbJyHvtk5D32ych77ZOQ99YAD+/6tQgKOFggADgAqjhYIAD4AOo4WCACSADqOZgQArADECAAEQEAAYABhYL/QACIBDmAYAAKOFggA6gA6jhYIAT4AOo5mBAFMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAGSADqOFggB6gA6jmYEAewAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIAj4AOo5mBAKMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAKSADqOFggC6gA6jmYEAywAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIAz4AOo4WCAOSADqOZgQDzADECAAEQEAAYABhYL/QACIBDmAYAAKOFggD6gA6jhYIBD4AOo5iBARsAEQIAARAQFGAAYWC/0AAiAQ5gGACjhYIBJIAOo4WCATqADqOZgQFDADECAAEQEAAYABhYL/QACIBDmAYAAKOFggFPgA6jhYIBZIAOo5mBAWsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAXqADqOFggGPgA6jmYEBkwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIBpIAOo4WCAbqADqOZgQG7ADECAAEQEAAYABhYL/QACIBDmAYAAKOFggHPgA6jmYEB4wAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIB5IAOo4WCAfqADqOZgQILADECAAEQEAAYABhYL/QACIBDmAYAAKOFggIPgA6jhYICJIAOo5mBAjMAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAjqADqOFggJPgA6jmYECWwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYICZIAOo4WCAnqADqOZgQKDADECAAEQEAAYABhYL/QACIBDmAYAAKOFggKPgA6jhYICpIAOo5mBAqsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCArqADqOFggLPgA6jmIEC0wARAgABEBAUYABhYL/QACIBDmAYAKOFggLkgA6jhYIC+oAOo5mBAvsAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCAw+ADqOZgQMjADECAAEQEAAYABhYL/QACIBDmAYAAKOFggMkgA6jhYIDOoAOo5mBA0sAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCA0+ADqOFggNkgA6jmYEDcwAxAgABEBAAGAAYWC/0AAiAQ5gGAACjhYIDeoAOo4WCA4+ADqOZgQObADECAAEQEAAYABhYL/QACIBDmAYAAKOFggOkgA6jhYIDuoAOo5mBA8MAMQIAARAQABgAGFgv9AAIgEOYBgAAo4WCA8+ADqOFggPkgA6jhYID+oAOo4WCBA+ADhxTu2sBAAAAAAAAEbuPs4EDt4r3gQHxghEr8IEK",mp4:"data:video/mp4;base64,AAAAHGZ0eXBNNFYgAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAGF21kYXTeBAAAbGliZmFhYyAxLjI4AABCAJMgBDIARwAAArEGBf//rdxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNDIgcjIgOTU2YzhkOCAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMTQgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDE6MHgxMTEgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCB2YnZfbWF4cmF0ZT03NjggdmJ2X2J1ZnNpemU9MzAwMCBjcmZfbWF4PTAuMCBuYWxfaHJkPW5vbmUgZmlsbGVyPTAgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAFZliIQL8mKAAKvMnJycnJycnJycnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXiEASZACGQAjgCEASZACGQAjgAAAAAdBmjgX4GSAIQBJkAIZACOAAAAAB0GaVAX4GSAhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGagC/AySEASZACGQAjgAAAAAZBmqAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZrAL8DJIQBJkAIZACOAAAAABkGa4C/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmwAvwMkhAEmQAhkAI4AAAAAGQZsgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGbQC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm2AvwMkhAEmQAhkAI4AAAAAGQZuAL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGboC/AySEASZACGQAjgAAAAAZBm8AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZvgL8DJIQBJkAIZACOAAAAABkGaAC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmiAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpAL8DJIQBJkAIZACOAAAAABkGaYC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmoAvwMkhAEmQAhkAI4AAAAAGQZqgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGawC/AySEASZACGQAjgAAAAAZBmuAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZsAL8DJIQBJkAIZACOAAAAABkGbIC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm0AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZtgL8DJIQBJkAIZACOAAAAABkGbgCvAySEASZACGQAjgCEASZACGQAjgAAAAAZBm6AnwMkhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AAAAhubW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAABDcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzB0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAA+kAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAALAAAACQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAPpAAAAAAABAAAAAAKobWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAB1MAAAdU5VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACU21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAhNzdGJsAAAAr3N0c2QAAAAAAAAAAQAAAJ9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAALAAkABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAALWF2Y0MBQsAN/+EAFWdCwA3ZAsTsBEAAAPpAADqYA8UKkgEABWjLg8sgAAAAHHV1aWRraEDyXyRPxbo5pRvPAyPzAAAAAAAAABhzdHRzAAAAAAAAAAEAAAAeAAAD6QAAABRzdHNzAAAAAAAAAAEAAAABAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAAIxzdHN6AAAAAAAAAAAAAAAeAAADDwAAAAsAAAALAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAiHN0Y28AAAAAAAAAHgAAAEYAAANnAAADewAAA5gAAAO0AAADxwAAA+MAAAP2AAAEEgAABCUAAARBAAAEXQAABHAAAASMAAAEnwAABLsAAATOAAAE6gAABQYAAAUZAAAFNQAABUgAAAVkAAAFdwAABZMAAAWmAAAFwgAABd4AAAXxAAAGDQAABGh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAABDcAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAQkAAADcAABAAAAAAPgbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAC7gAAAykBVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAADi21pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAADT3N0YmwAAABnc3RzZAAAAAAAAAABAAAAV21wNGEAAAAAAAAAAQAAAAAAAAAAAAIAEAAAAAC7gAAAAAAAM2VzZHMAAAAAA4CAgCIAAgAEgICAFEAVBbjYAAu4AAAADcoFgICAAhGQBoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAADIAAAQAAAAAAQAAAkAAAAFUc3RzYwAAAAAAAAAbAAAAAQAAAAEAAAABAAAAAgAAAAIAAAABAAAAAwAAAAEAAAABAAAABAAAAAIAAAABAAAABgAAAAEAAAABAAAABwAAAAIAAAABAAAACAAAAAEAAAABAAAACQAAAAIAAAABAAAACgAAAAEAAAABAAAACwAAAAIAAAABAAAADQAAAAEAAAABAAAADgAAAAIAAAABAAAADwAAAAEAAAABAAAAEAAAAAIAAAABAAAAEQAAAAEAAAABAAAAEgAAAAIAAAABAAAAFAAAAAEAAAABAAAAFQAAAAIAAAABAAAAFgAAAAEAAAABAAAAFwAAAAIAAAABAAAAGAAAAAEAAAABAAAAGQAAAAIAAAABAAAAGgAAAAEAAAABAAAAGwAAAAIAAAABAAAAHQAAAAEAAAABAAAAHgAAAAIAAAABAAAAHwAAAAQAAAABAAAA4HN0c3oAAAAAAAAAAAAAADMAAAAaAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAACMc3RjbwAAAAAAAAAfAAAALAAAA1UAAANyAAADhgAAA6IAAAO+AAAD0QAAA+0AAAQAAAAEHAAABC8AAARLAAAEZwAABHoAAASWAAAEqQAABMUAAATYAAAE9AAABRAAAAUjAAAFPwAABVIAAAVuAAAFgQAABZ0AAAWwAAAFzAAABegAAAX7AAAGFwAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTUuMzMuMTAw"}}])})); diff --git a/public_included_ws_fallback/scripts/QRCode.min.js b/public_included_ws_fallback/scripts/QRCode.min.js new file mode 100644 index 0000000..569a867 --- /dev/null +++ b/public_included_ws_fallback/scripts/QRCode.min.js @@ -0,0 +1,2 @@ +/*! qrcode-svg v1.1.0 | https://github.com/papnkukn/qrcode-svg | MIT license */ +function QR8bitByte(t){this.mode=QRMode.MODE_8BIT_BYTE,this.data=t,this.parsedData=[];for(var e=0,r=this.data.length;e65536?(o[0]=240|(1835008&n)>>>18,o[1]=128|(258048&n)>>>12,o[2]=128|(4032&n)>>>6,o[3]=128|63&n):n>2048?(o[0]=224|(61440&n)>>>12,o[1]=128|(4032&n)>>>6,o[2]=128|63&n):n>128?(o[0]=192|(1984&n)>>>6,o[1]=128|63&n):o[0]=n,this.parsedData.push(o)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function QRCodeModel(t,e){this.typeNumber=t,this.errorCorrectLevel=e,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}QR8bitByte.prototype={getLength:function(t){return this.parsedData.length},write:function(t){for(var e=0,r=this.parsedData.length;e=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,e)},setupPositionProbePattern:function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(this.modules[t+r][e+o]=0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4)},getBestMaskPattern:function(){for(var t=0,e=0,r=0;r<8;r++){this.makeImpl(!0,r);var o=QRUtil.getLostPoint(this);(0==r||t>o)&&(t=o,e=r)}return e},createMovieClip:function(t,e,r){var o=t.createEmptyMovieClip(e,r);this.make();for(var n=0;n>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++){o=!t&&1==(e>>r&1);this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o}},setupTypeInfo:function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=QRUtil.getBCHTypeInfo(r),n=0;n<15;n++){var i=!t&&1==(o>>n&1);n<6?this.modules[n][8]=i:n<8?this.modules[n+1][8]=i:this.modules[this.moduleCount-15+n][8]=i}for(n=0;n<15;n++){i=!t&&1==(o>>n&1);n<8?this.modules[8][this.moduleCount-n-1]=i:n<9?this.modules[8][15-n-1+1]=i:this.modules[8][15-n-1]=i}this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,n=7,i=0,a=this.moduleCount-1;a>0;a-=2)for(6==a&&a--;;){for(var s=0;s<2;s++)if(null==this.modules[o][a-s]){var h=!1;i>>n&1)),QRUtil.getMask(e,o,a-s)&&(h=!h),this.modules[o][a-s]=h,-1==--n&&(i++,n=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},QRCodeModel.PAD0=236,QRCodeModel.PAD1=17,QRCodeModel.createData=function(t,e,r){for(var o=QRRSBlock.getRSBlocks(t,e),n=new QRBitBuffer,i=0;i8*s)throw new Error("code length overflow. ("+n.getLengthInBits()+">"+8*s+")");for(n.getLengthInBits()+4<=8*s&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(!1);for(;!(n.getLengthInBits()>=8*s||(n.put(QRCodeModel.PAD0,8),n.getLengthInBits()>=8*s));)n.put(QRCodeModel.PAD1,8);return QRCodeModel.createBytes(n,o)},QRCodeModel.createBytes=function(t,e){for(var r=0,o=0,n=0,i=new Array(e.length),a=new Array(e.length),s=0;s=0?d.get(f):0}}var c=0;for(u=0;u=0;)e^=QRUtil.G15<=0;)e^=QRUtil.G18<>>=1;return e},getPatternPosition:function(t){return QRUtil.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case QRMaskPattern.PATTERN000:return(e+r)%2==0;case QRMaskPattern.PATTERN001:return e%2==0;case QRMaskPattern.PATTERN010:return r%3==0;case QRMaskPattern.PATTERN011:return(e+r)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(e/2)+Math.floor(r/3))%2==0;case QRMaskPattern.PATTERN101:return e*r%2+e*r%3==0;case QRMaskPattern.PATTERN110:return(e*r%2+e*r%3)%2==0;case QRMaskPattern.PATTERN111:return(e*r%3+(e+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new QRPolynomial([1],0),r=0;r5&&(r+=3+i-5)}for(o=0;o=256;)t-=255;return QRMath.EXP_TABLE[t]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},i=0;i<8;i++)QRMath.EXP_TABLE[i]=1<>>7-t%8&1)},put:function(t,e){for(var r=0;r>>e-r-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];function QRCode(t){if(this.options={padding:4,width:256,height:256,typeNumber:4,color:"#000000",background:"#ffffff",ecl:"M"},"string"==typeof t&&(t={content:t}),t)for(var e in t)this.options[e]=t[e];if("string"!=typeof this.options.content)throw new Error("Expected 'content' as string!");if(0===this.options.content.length)throw new Error("Expected 'content' to be non-empty!");if(!(this.options.padding>=0))throw new Error("Expected 'padding' value to be non-negative!");if(!(this.options.width>0&&this.options.height>0))throw new Error("Expected 'width' or 'height' value to be higher than zero!");var r=this.options.content,o=function(t,e){for(var r=function(t){var e=encodeURI(t).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return e.length+(e.length!=t?3:0)}(t),o=1,n=0,i=0,a=QRCodeLimitLength.length;i<=a;i++){var s=QRCodeLimitLength[i];if(!s)throw new Error("Content too long: expected "+n+" but got "+r);switch(e){case"L":n=s[0];break;case"M":n=s[1];break;case"Q":n=s[2];break;case"H":n=s[3];break;default:throw new Error("Unknwon error correction level: "+e)}if(r<=n)break;o++}if(o>QRCodeLimitLength.length)throw new Error("Content too long");return o}(r,this.options.ecl),n=function(t){switch(t){case"L":return QRErrorCorrectLevel.L;case"M":return QRErrorCorrectLevel.M;case"Q":return QRErrorCorrectLevel.Q;case"H":return QRErrorCorrectLevel.H;default:throw new Error("Unknwon error correction level: "+t)}}(this.options.ecl);this.qrcode=new QRCodeModel(o,n),this.qrcode.addData(r),this.qrcode.make()}QRCode.prototype.svg=function(t){var e=this.options||{},r=this.qrcode.modules;void 0===t&&(t={container:e.container||"svg"});for(var o=void 0===e.pretty||!!e.pretty,n=o?" ":"",i=o?"\r\n":"",a=e.width,s=e.height,h=r.length,l=a/(h+2*e.padding),u=s/(h+2*e.padding),g=void 0!==e.join&&!!e.join,d=void 0!==e.swap&&!!e.swap,f=void 0===e.xmlDeclaration||!!e.xmlDeclaration,c=void 0!==e.predefined&&!!e.predefined,R=c?n+''+i:"",p=n+''+i,m="",Q="",v=0;v'+i:n+''+i}}g&&(m=n+'');var T="";switch(t.container){case"svg":f&&(T+=''+i),T+=''+i,T+=R+p+m,T+="";break;case"svg-viewbox":f&&(T+=''+i),T+=''+i,T+=R+p+m,T+="";break;case"g":T+=''+i,T+=R+p+m,T+="";break;default:T+=(R+p+m).replace(/^\s+/,"")}return T},QRCode.prototype.save=function(t,e){var r=this.svg();"function"!=typeof e&&(e=function(t,e){});try{require("fs").writeFile(t,r,e)}catch(t){e(t)}},"undefined"!=typeof module&&(module.exports=QRCode); \ No newline at end of file diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js new file mode 100644 index 0000000..67452cc --- /dev/null +++ b/public_included_ws_fallback/scripts/network.js @@ -0,0 +1,1153 @@ +window.URL = window.URL || window.webkitURL; +window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); + +window.hiddenProperty = 'hidden' in document ? 'hidden' : + 'webkitHidden' in document ? 'webkitHidden' : + 'mozHidden' in document ? 'mozHidden' : + null; +window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' : + 'webkitvisibilitychange' in document ? 'webkitvisibilitychange' : + 'mozvisibilitychange' in document ? 'mozvisibilitychange' : + null; + +class ServerConnection { + + constructor() { + this._connect(); + Events.on('pagehide', _ => this._disconnect()); + document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange()); + if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); + Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail })); + Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'})); + Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail})); + Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail})); + Events.on('resend-peers', _ => this.send({ type: 'resend-peers'})); + Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate()); + Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail)); + Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' })); + Events.on('offline', _ => clearTimeout(this._reconnectTimer)); + Events.on('online', _ => this._connect()); + } + + _connect() { + clearTimeout(this._reconnectTimer); + if (this._isConnected() || this._isConnecting()) return; + const ws = new WebSocket(this._endpoint()); + ws.binaryType = 'arraybuffer'; + ws.onopen = _ => this._onOpen(); + ws.onmessage = e => this._onMessage(e.data); + ws.onclose = _ => this._onDisconnect(); + ws.onerror = e => this._onError(e); + this._socket = ws; + } + + _onOpen() { + console.log('WS: server connected'); + Events.fire('ws-connected'); + if (this._isReconnect) Events.fire('notify-user', 'Connected.'); + } + + _onPairDeviceInitiate() { + if (!this._isConnected()) { + Events.fire('notify-user', 'You need to be online to pair devices.'); + return; + } + this.send({ type: 'pair-device-initiate' }) + } + + _onPairDeviceJoin(roomKey) { + if (!this._isConnected()) { + setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000); + return; + } + this.send({ type: 'pair-device-join', roomKey: roomKey }) + } + + _setRtcConfig(config) { + window.rtcConfig = config; + } + + _onMessage(msg) { + msg = JSON.parse(msg); + if (msg.type !== 'ping') console.log('WS receive:', msg); + switch (msg.type) { + case 'rtc-config': + this._setRtcConfig(msg.config); + break; + case 'peers': + this._onPeers(msg); + break; + case 'peer-joined': + Events.fire('peer-joined', msg); + break; + case 'peer-left': + Events.fire('peer-left', msg); + break; + case 'signal': + Events.fire('signal', msg); + break; + case 'ping': + this.send({ type: 'pong' }); + break; + case 'display-name': + this._onDisplayName(msg); + break; + case 'pair-device-initiated': + Events.fire('pair-device-initiated', msg); + break; + case 'pair-device-joined': + Events.fire('pair-device-joined', msg); + break; + case 'pair-device-join-key-invalid': + Events.fire('pair-device-join-key-invalid'); + break; + case 'pair-device-canceled': + Events.fire('pair-device-canceled', msg.roomKey); + break; + case 'pair-device-join-key-rate-limit': + Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.'); + break; + case 'secret-room-deleted': + Events.fire('secret-room-deleted', msg.roomSecret); + break; + case 'room-secret-regenerated': + Events.fire('room-secret-regenerated', msg); + break; + case 'request': + case 'header': + case 'partition': + case 'partition-received': + case 'progress': + case 'files-transfer-response': + case 'file-transfer-complete': + case 'message-transfer-complete': + case 'text': + case 'display-name-changed': + case 'ws-chunk': + Events.fire('ws-relay', JSON.stringify(msg)); + break; + default: + console.error('WS receive: unknown message type', msg); + } + } + + send(msg) { + if (!this._isConnected()) return; + if (msg.type !== 'pong') console.log("WS send:", msg) + this._socket.send(JSON.stringify(msg)); + } + + _onPeers(msg) { + Events.fire('peers', msg); + if (msg.roomType === "ip" && msg.peers.length === 0) { + BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => { + if (!peerId) return; + console.log("successfully removed other peerIds from localStorage"); + }); + } + } + + _onDisplayName(msg) { + // Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload + sessionStorage.setItem("peerId", msg.message.peerId); + sessionStorage.setItem("peerIdHash", msg.message.peerIdHash); + + // Add peerId to localStorage to mark it for other MoreLoad tabs on the same browser + BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => { + if (!peerId) return; + console.log("successfully added peerId to localStorage"); + + // Only now join rooms + Events.fire('join-ip-room'); + PersistentStorage.getAllRoomSecrets().then(roomSecrets => { + Events.fire('room-secrets', roomSecrets); + }); + }); + + Events.fire('display-name', msg); + } + + _endpoint() { + // hack to detect if deployment or development environment + const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws'; + const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback'; + let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc); + const peerId = sessionStorage.getItem("peerId"); + const peerIdHash = sessionStorage.getItem("peerIdHash"); + if (peerId && peerIdHash) { + ws_url.searchParams.append('peer_id', peerId); + ws_url.searchParams.append('peer_id_hash', peerIdHash); + } + return ws_url.toString(); + } + + _disconnect() { + this.send({ type: 'disconnect' }); + + const peerId = sessionStorage.getItem("peerId"); + BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => { + console.log("successfully removed peerId from localStorage"); + }); + + if (!this._socket) return; + + this._socket.onclose = null; + this._socket.close(); + this._socket = null; + Events.fire('ws-disconnected'); + this._isReconnect = true; + } + + _onDisconnect() { + console.log('WS: server disconnected'); + Events.fire('notify-user', 'Connecting..'); + clearTimeout(this._reconnectTimer); + this._reconnectTimer = setTimeout(_ => this._connect(), 1000); + Events.fire('ws-disconnected'); + this._isReconnect = true; + } + + _onVisibilityChange() { + if (window.hiddenProperty) return; + this._connect(); + } + + _isConnected() { + return this._socket && this._socket.readyState === this._socket.OPEN; + } + + _isConnecting() { + return this._socket && this._socket.readyState === this._socket.CONNECTING; + } + + _onError(e) { + console.error(e); + } + + _reconnect() { + this._disconnect(); + this._connect(); + } +} + +class Peer { + + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { + this._server = serverConnection; + this._isCaller = isCaller; + this._peerId = peerId; + this._roomType = roomType; + this._updateRoomSecret(roomSecret); + + this._filesQueue = []; + this._busy = false; + + // evaluate auto accept + this._evaluateAutoAccept(); + } + + sendJSON(message) { + this._send(JSON.stringify(message)); + } + + sendDisplayName(displayName) { + this.sendJSON({type: 'display-name-changed', displayName: displayName}); + } + + _isSameBrowser() { + return BrowserTabsConnector.peerIsSameBrowser(this._peerId); + } + + _updateRoomSecret(roomSecret) { + // if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets + // -> do not delete duplicates and do not regenerate room secrets + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) { + // remove old roomSecrets to prevent multiple pairings with same peer + PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => { + if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret); + }) + } + + this._roomSecret = roomSecret; + + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) { + // increase security by increasing roomSecret length + console.log('RoomSecret is regenerated to increase security') + Events.fire('regenerate-room-secret', this._roomSecret); + } + } + + _evaluateAutoAccept() { + if (!this._roomSecret) { + this._setAutoAccept(false); + return; + } + + PersistentStorage.getRoomSecretEntry(this._roomSecret) + .then(roomSecretEntry => { + const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false; + this._setAutoAccept(autoAccept); + }) + .catch(_ => { + this._setAutoAccept(false); + }); + } + + _setAutoAccept(autoAccept) { + this._autoAccept = autoAccept; + } + + getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) { + return new Promise((resolve, reject) => { + let image = new Image(); + image.src = URL.createObjectURL(file); + image.onload = _ => { + let imageWidth = image.width; + let imageHeight = image.height; + let canvas = document.createElement('canvas'); + + // resize the canvas and draw the image data into it + if (width && height) { + canvas.width = width; + canvas.height = height; + } else if (width) { + canvas.width = width; + canvas.height = Math.floor(imageHeight * width / imageWidth) + } else if (height) { + canvas.width = Math.floor(imageWidth * height / imageHeight); + canvas.height = height; + } else { + canvas.width = imageWidth; + canvas.height = imageHeight + } + + var ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0, canvas.width, canvas.height); + + let dataUrl = canvas.toDataURL("image/jpeg", quality); + resolve(dataUrl); + } + image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`); + }).then(dataUrl => { + return dataUrl; + }).catch(e => console.error(e)); + } + + async requestFileTransfer(files) { + let header = []; + let totalSize = 0; + let imagesOnly = true + for (let i=0; i this._send(chunk), + offset => this._onPartitionEnd(offset)); + this._chunker.nextPartition(); + } + + _onPartitionEnd(offset) { + this.sendJSON({ type: 'partition', offset: offset }); + } + + _onReceivedPartitionEnd(offset) { + this.sendJSON({ type: 'partition-received', offset: offset }); + } + + _sendNextPartition() { + if (!this._chunker || this._chunker.isFileEnd()) return; + this._chunker.nextPartition(); + } + + _sendProgress(progress) { + this.sendJSON({ type: 'progress', progress: progress }); + } + + _onMessage(message) { + if (typeof message !== 'string') { + this._onChunkReceived(message); + return; + } + const messageJSON = JSON.parse(message); + switch (messageJSON.type) { + case 'request': + this._onFilesTransferRequest(messageJSON); + break; + case 'header': + this._onFileHeader(messageJSON); + break; + case 'partition': + this._onReceivedPartitionEnd(messageJSON); + break; + case 'partition-received': + this._sendNextPartition(); + break; + case 'progress': + this._onDownloadProgress(messageJSON.progress); + break; + case 'files-transfer-response': + this._onFileTransferRequestResponded(messageJSON); + break; + case 'file-transfer-complete': + this._onFileTransferCompleted(); + break; + case 'message-transfer-complete': + this._onMessageTransferCompleted(); + break; + case 'text': + this._onTextReceived(messageJSON); + break; + case 'display-name-changed': + this._onDisplayNameChanged(messageJSON); + break; + } + } + + _onFilesTransferRequest(request) { + if (this._requestPending) { + // Only accept one request at a time per peer + this.sendJSON({type: 'files-transfer-response', accepted: false}); + return; + } + if (window.iOS && request.totalSize >= 200*1024*1024) { + // iOS Safari can only put 400MB at once to memory. + // Request to send them in chunks of 200MB instead: + this.sendJSON({type: 'files-transfer-response', accepted: false, reason: 'ios-memory-limit'}); + return; + } + + this._requestPending = request; + + if (this._autoAccept) { + // auto accept if set via Edit Paired Devices Dialog + this._respondToFileTransferRequest(true); + return; + } + + // default behavior: show user transfer request + Events.fire('files-transfer-request', { + request: request, + peerId: this._peerId + }); + } + + _respondToFileTransferRequest(accepted) { + this.sendJSON({type: 'files-transfer-response', accepted: accepted}); + if (accepted) { + this._requestAccepted = this._requestPending; + this._totalBytesReceived = 0; + this._busy = true; + this._filesReceived = []; + } + this._requestPending = null; + } + + _onFileHeader(header) { + if (this._requestAccepted && this._requestAccepted.header.length) { + this._lastProgress = 0; + this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, + this._requestAccepted.totalSize, + this._totalBytesReceived, + fileBlob => this._onFileReceived(fileBlob) + ); + } + } + + _abortTransfer() { + Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); + Events.fire('notify-user', 'Files are incorrect.'); + this._filesReceived = []; + this._requestAccepted = null; + this._digester = null; + throw new Error("Received files differ from requested files. Abort!"); + } + + _onChunkReceived(chunk) { + if(!this._digester || !(chunk.byteLength || chunk.size)) return; + + this._digester.unchunk(chunk); + const progress = this._digester.progress; + + if (progress > 1) { + this._abortTransfer(); + } + + this._onDownloadProgress(progress); + + // occasionally notify sender about our progress + if (progress - this._lastProgress < 0.005 && progress !== 1) return; + this._lastProgress = progress; + this._sendProgress(progress); + } + + _onDownloadProgress(progress) { + Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'}); + } + + async _onFileReceived(fileBlob) { + const acceptedHeader = this._requestAccepted.header.shift(); + this._totalBytesReceived += fileBlob.size; + + this.sendJSON({type: 'file-transfer-complete'}); + + const sameSize = fileBlob.size === acceptedHeader.size; + const sameName = fileBlob.name === acceptedHeader.name + if (!sameSize || !sameName) { + this._abortTransfer(); + } + + // include for compatibility with Snapdrop for Android app + Events.fire('file-received', fileBlob); + + this._filesReceived.push(fileBlob); + if (!this._requestAccepted.header.length) { + this._busy = false; + Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); + Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize}); + this._filesReceived = []; + this._requestAccepted = null; + } + } + + _onFileTransferCompleted() { + this._chunker = null; + if (!this._filesQueue.length) { + this._busy = false; + Events.fire('notify-user', 'File transfer completed.'); + } else { + this._dequeueFile(); + } + } + + _onFileTransferRequestResponded(message) { + if (!message.accepted) { + Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); + this._filesRequested = null; + if (message.reason === 'ios-memory-limit') { + Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); + } + return; + } + Events.fire('file-transfer-accepted'); + Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'transfer'}); + this.sendFiles(); + } + + _onMessageTransferCompleted() { + Events.fire('notify-user', 'Message transfer completed.'); + } + + sendText(text) { + const unescaped = btoa(unescape(encodeURIComponent(text))); + this.sendJSON({ type: 'text', text: unescaped }); + } + + _onTextReceived(message) { + if (!message.text) return; + const escaped = decodeURIComponent(escape(atob(message.text))); + Events.fire('text-received', { text: escaped, peerId: this._peerId }); + this.sendJSON({ type: 'message-transfer-complete' }); + } + + _onDisplayNameChanged(message) { + const displayNameHasChanged = this._displayName !== message.displayName + + if (message.displayName && displayNameHasChanged) { + this._displayName = message.displayName; + } + + Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); + + if (!displayNameHasChanged) return; + Events.fire('notify-peer-display-name-changed', this._peerId); + } +} + +class RTCPeer extends Peer { + + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { + super(serverConnection, isCaller, peerId, roomType, roomSecret); + this.rtcSupported = true; + if (!this._isCaller) return; // we will listen for a caller + this._connect(); + } + + _connect() { + if (!this._conn || this._conn.signalingState === "closed") this._openConnection(); + + if (this._isCaller) { + this._openChannel(); + } else { + this._conn.ondatachannel = e => this._onChannelOpened(e); + } + } + + _openConnection() { + this._conn = new RTCPeerConnection(window.rtcConfig); + this._conn.onicecandidate = e => this._onIceCandidate(e); + this._conn.onicecandidateerror = e => this._onError(e); + this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); + this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); + } + + _openChannel() { + if (!this._conn) return; + const channel = this._conn.createDataChannel('data-channel', { + ordered: true, + reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable + }); + channel.onopen = e => this._onChannelOpened(e); + channel.onerror = e => this._onError(e); + this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e)); + } + + _onDescription(description) { + // description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400'); + this._conn.setLocalDescription(description) + .then(_ => this._sendSignal({ sdp: description })) + .catch(e => this._onError(e)); + } + + _onIceCandidate(event) { + if (!event.candidate) return; + this._sendSignal({ ice: event.candidate }); + } + + onServerMessage(message) { + if (!this._conn) this._connect(); + + if (message.sdp) { + this._conn.setRemoteDescription(message.sdp) + .then( _ => { + if (message.sdp.type === 'offer') { + return this._conn.createAnswer() + .then(d => this._onDescription(d)); + } + }) + .catch(e => this._onError(e)); + } else if (message.ice) { + this._conn.addIceCandidate(new RTCIceCandidate(message.ice)) + .catch(e => this._onError(e)); + } + } + + _onChannelOpened(event) { + console.log('RTC: channel opened with', this._peerId); + const channel = event.channel || event.target; + channel.binaryType = 'arraybuffer'; + channel.onmessage = e => this._onMessage(e.data); + channel.onclose = _ => this._onChannelClosed(); + this._channel = channel; + Events.on('beforeunload', e => this._onBeforeUnload(e)); + Events.on('pagehide', _ => this._onPageHide()); + Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()}); + } + + _onMessage(message) { + if (typeof message === 'string') { + console.log('RTC:', JSON.parse(message)); + } + super._onMessage(message); + } + + getConnectionHash() { + const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); + const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); + let localConnectionFingerprint, remoteConnectionFingerprint; + for (let i=0; i this._onMessage(e.detail)); + Events.on('peers', e => this._onPeers(e.detail)); + Events.on('files-selected', e => this._onFilesSelected(e.detail)); + Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) + Events.on('send-text', e => this._onSendText(e.detail)); + Events.on('peer-left', e => this._onPeerLeft(e.detail)); + Events.on('peer-joined', e => this._onPeerJoined(e.detail)); + Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId)); + Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); + Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail)); + Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); + Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); + Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail)); + Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept)); + Events.on('ws-disconnected', _ => this._onWsDisconnected()); + Events.on('ws-relay', e => this._onWsRelay(e.detail)); + } + + _onMessage(message) { + const peerId = message.sender.id; + this.peers[peerId].onServerMessage(message); + } + + _refreshPeer(peer, roomType, roomSecret) { + if (!peer) return false; + + const roomTypeIsSecret = roomType === "secret"; + const roomSecretsDiffer = peer._roomSecret !== roomSecret; + + // if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept + if (roomTypeIsSecret && roomSecretsDiffer) { + peer._updateRoomSecret(roomSecret); + peer._evaluateAutoAccept(); + + return true; + } + + const roomTypesDiffer = peer._roomType !== roomType; + + // if roomTypes differ peer is already connected -> abort + if (roomTypesDiffer) return true; + + peer.refresh(); + + return true; + } + + _createOrRefreshPeer(isCaller, peerId, roomType, roomSecret, rtcSupported) { + const peer = this.peers[peerId]; + if (peer) { + this._refreshPeer(peer, roomType, roomSecret); + return; + } + + if (window.isRtcSupported && rtcSupported) { + this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret); + } else { + this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret); + } + } + + _onPeerJoined(message) { + this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported); + } + + _onPeers(message) { + message.peers.forEach(peer => { + this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported); + }) + } + + _onWsRelay(message) { + const messageJSON = JSON.parse(message); + if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk); + this.peers[messageJSON.sender.id]._onMessage(message); + } + + _onRespondToFileTransferRequest(detail) { + this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); + } + + _onFilesSelected(message) { + let inputFiles = Array.from(message.files); + delete message.files; + let files = []; + const l = inputFiles.length; + for (let i=0; i { + if (!peerIds) return; + console.log("successfully removed other peerIds from localStorage"); + }); + } + } + } + + _onPeerConnected(peerId) { + this._notifyPeerDisplayNameChanged(peerId); + } + + _onWsDisconnected() { + for (const peerId in this.peers) { + if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) { + Events.fire('peer-disconnected', peerId); + } + } + } + + _onPeerDisconnected(peerId) { + const peer = this.peers[peerId]; + delete this.peers[peerId]; + if (!peer || !peer._conn) return; + if (peer._channel) peer._channel.onclose = null; + peer._conn.close(); + peer._busy = false; + } + + _onSecretRoomDeleted(roomSecret) { + for (const peerId in this.peers) { + const peer = this.peers[peerId]; + if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) { + this._onPeerDisconnected(peerId); + } + } + } + + _onRoomSecretRegenerated(message) { + PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => { + console.log("successfully regenerated room secret"); + Events.fire("room-secrets", [message.newRoomSecret]); + }) + } + + _notifyPeersDisplayNameChanged(newDisplayName) { + this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName; + for (const peerId in this.peers) { + this._notifyPeerDisplayNameChanged(peerId); + } + } + + _notifyPeerDisplayNameChanged(peerId) { + const peer = this.peers[peerId]; + if (!peer) return; + this.peers[peerId].sendDisplayName(this._displayName); + } + + _onDisplayName(displayName) { + this._originalDisplayName = displayName; + // if the displayName has not been changed (yet) set the displayName to the original displayName + if (!this._displayName) this._displayName = displayName; + } + + _onAutoAcceptUpdated(roomSecret, autoAccept) { + const peerId = this._getPeerIdFromRoomSecret(roomSecret); + if (!peerId) return; + this.peers[peerId]._setAutoAccept(autoAccept); + } + + _getPeerIdFromRoomSecret(roomSecret) { + for (const peerId in this.peers) { + const peer = this.peers[peerId]; + // peer must have same roomSecret and not be on the same browser. + if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) { + return peer._peerId; + } + } + return false; + } +} + +class FileChunker { + + constructor(file, onChunk, onPartitionEnd) { + this._chunkSize = 64000; // 64 KB + this._maxPartitionSize = 1e6; // 1 MB + this._offset = 0; + this._partitionSize = 0; + this._file = file; + this._onChunk = onChunk; + this._onPartitionEnd = onPartitionEnd; + this._reader = new FileReader(); + this._reader.addEventListener('load', e => this._onChunkRead(e.target.result)); + } + + nextPartition() { + this._partitionSize = 0; + this._readChunk(); + } + + _readChunk() { + const chunk = this._file.slice(this._offset, this._offset + this._chunkSize); + this._reader.readAsArrayBuffer(chunk); + } + + _onChunkRead(chunk) { + this._offset += chunk.byteLength; + this._partitionSize += chunk.byteLength; + this._onChunk(chunk); + if (this.isFileEnd()) return; + if (this._isPartitionEnd()) { + this._onPartitionEnd(this._offset); + return; + } + this._readChunk(); + } + + repeatPartition() { + this._offset -= this._partitionSize; + this.nextPartition(); + } + + _isPartitionEnd() { + return this._partitionSize >= this._maxPartitionSize; + } + + isFileEnd() { + return this._offset >= this._file.size; + } +} + +class FileDigester { + + constructor(meta, totalSize, totalBytesReceived, callback) { + this._buffer = []; + this._bytesReceived = 0; + this._size = meta.size; + this._name = meta.name; + this._mime = meta.mime; + this._totalSize = totalSize; + this._totalBytesReceived = totalBytesReceived; + this._callback = callback; + } + + unchunk(chunk) { + this._buffer.push(chunk); + this._bytesReceived += chunk.byteLength || chunk.size; + this.progress = (this._totalBytesReceived + this._bytesReceived) / this._totalSize; + if (isNaN(this.progress)) this.progress = 1 + + if (this._bytesReceived < this._size) return; + // we are done + const blob = new Blob(this._buffer) + this._buffer = null; + this._callback(new File([blob], this._name, { + type: this._mime, + lastModified: new Date().getTime() + })); + } + +} + +class Events { + static fire(type, detail) { + window.dispatchEvent(new CustomEvent(type, { detail: detail })); + } + + static on(type, callback, options = false) { + return window.addEventListener(type, callback, options); + } + + static off(type, callback, options = false) { + return window.removeEventListener(type, callback, options); + } +} diff --git a/public_included_ws_fallback/scripts/theme.js b/public_included_ws_fallback/scripts/theme.js new file mode 100644 index 0000000..81cf0b2 --- /dev/null +++ b/public_included_ws_fallback/scripts/theme.js @@ -0,0 +1,78 @@ +(function(){ + + const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches; + + const $themeAuto = document.getElementById('theme-auto'); + const $themeLight = document.getElementById('theme-light'); + const $themeDark = document.getElementById('theme-dark'); + + let currentTheme = localStorage.getItem('theme'); + + if (currentTheme === 'dark') { + setModeToDark(); + } else if (currentTheme === 'light') { + setModeToLight(); + } + + $themeAuto.addEventListener('click', _ => { + if (currentTheme) { + setModeToAuto(); + } else { + setModeToDark(); + } + }); + $themeLight.addEventListener('click', _ => { + if (currentTheme !== 'light') { + setModeToLight(); + } else { + setModeToAuto(); + } + }); + $themeDark.addEventListener('click', _ => { + if (currentTheme !== 'dark') { + setModeToDark(); + } else { + setModeToLight(); + } + }); + + function setModeToDark() { + document.body.classList.remove('light-theme'); + document.body.classList.add('dark-theme'); + localStorage.setItem('theme', 'dark'); + currentTheme = 'dark'; + + $themeAuto.classList.remove("selected"); + $themeLight.classList.remove("selected"); + $themeDark.classList.add("selected"); + } + + function setModeToLight() { + document.body.classList.remove('dark-theme'); + document.body.classList.add('light-theme'); + localStorage.setItem('theme', 'light'); + currentTheme = 'light'; + + $themeAuto.classList.remove("selected"); + $themeLight.classList.add("selected"); + $themeDark.classList.remove("selected"); + } + + function setModeToAuto() { + document.body.classList.remove('dark-theme'); + document.body.classList.remove('light-theme'); + if (prefersDarkTheme) { + document.body.classList.add('dark-theme'); + } else if (prefersLightTheme) { + document.body.classList.add('light-theme'); + } + localStorage.removeItem('theme'); + currentTheme = undefined; + + $themeAuto.classList.add("selected"); + $themeLight.classList.remove("selected"); + $themeDark.classList.remove("selected"); + } + +})(); diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js new file mode 100644 index 0000000..7d75289 --- /dev/null +++ b/public_included_ws_fallback/scripts/ui.js @@ -0,0 +1,2314 @@ +const $ = query => document.getElementById(query); +const $$ = query => document.body.querySelector(query); +window.isProductionEnvironment = !window.location.host.startsWith('localhost'); +window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; +window.android = /android/i.test(navigator.userAgent); +window.pasteMode = {}; +window.pasteMode.activated = false; + +// set display name +Events.on('display-name', e => { + const me = e.detail.message; + const $displayName = $('display-name'); + $displayName.setAttribute('placeholder', me.displayName); +}); + +class PeersUI { + + constructor() { + Events.on('peer-joined', e => this._onPeerJoined(e.detail)); + Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash)); + Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); + Events.on('peers', e => this._onPeers(e.detail)); + Events.on('set-progress', e => this._onSetProgress(e.detail)); + Events.on('paste', e => this._onPaste(e)); + Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); + this.peers = {}; + + this.$cancelPasteModeBtn = $('cancel-paste-mode'); + this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); + + Events.on('dragover', e => this._onDragOver(e)); + Events.on('dragleave', _ => this._onDragEnd()); + Events.on('dragend', _ => this._onDragEnd()); + + Events.on('drop', e => this._onDrop(e)); + Events.on('keydown', e => this._onKeyDown(e)); + + this.$xPeers = $$('x-peers'); + this.$xNoPeers = $$('x-no-peers'); + this.$xInstructions = $$('x-instructions'); + + Events.on('peer-added', _ => this.evaluateOverflowing()); + Events.on('bg-resize', _ => this.evaluateOverflowing()); + + this.$displayName = $('display-name'); + + this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e)); + this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e)); + this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText)); + + Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail)); + Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e)); + + // Load saved display name on page load + this._getSavedDisplayName().then(displayName => { + console.log("Retrieved edited display name:", displayName) + if (displayName) Events.fire('self-display-name-changed', displayName); + }); + + + /* prevent animation on load */ + setTimeout(_ => { + this.$xNoPeers.style.animationIterationCount = "1"; + }, 300); + } + + _insertDisplayName(displayName) { + this.$displayName.textContent = displayName; + } + + _onKeyDownDisplayName(e) { + if (e.key === "Enter" || e.key === "Escape") { + e.preventDefault(); + e.target.blur(); + } + } + + _onKeyUpDisplayName(e) { + // fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty + if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = ''; + } + + async _saveDisplayName(newDisplayName) { + newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') + const savedDisplayName = await this._getSavedDisplayName(); + if (newDisplayName === savedDisplayName) return; + + if (newDisplayName) { + PersistentStorage.set('editedDisplayName', newDisplayName) + .then(_ => { + Events.fire('notify-user', 'Device name is changed permanently.'); + }) + .catch(_ => { + console.log("This browser does not support IndexedDB. Use localStorage instead."); + localStorage.setItem('editedDisplayName', newDisplayName); + Events.fire('notify-user', 'Device name is changed only for this session.'); + }) + .finally(_ => { + Events.fire('self-display-name-changed', newDisplayName); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName}); + }); + } else { + PersistentStorage.delete('editedDisplayName') + .catch(_ => { + console.log("This browser does not support IndexedDB. Use localStorage instead.") + localStorage.removeItem('editedDisplayName'); + Events.fire('notify-user', 'Random Display name is used again.'); + }) + .finally(_ => { + Events.fire('notify-user', 'Device name is randomly generated again.'); + Events.fire('self-display-name-changed', ''); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); + }); + } + } + + _getSavedDisplayName() { + return new Promise((resolve) => { + PersistentStorage.get('editedDisplayName') + .then(displayName => { + if (!displayName) displayName = ""; + resolve(displayName); + }) + .catch(_ => { + let displayName = localStorage.getItem('editedDisplayName'); + if (!displayName) displayName = ""; + resolve(displayName); + }) + }); + } + + _changePeerDisplayName(peerId, displayName) { + this.peers[peerId].name.displayName = displayName; + const peerIdNode = $(peerId); + if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; + this._redrawPeerRoomTypes(peerId); + } + + _onPeerDisplayNameChanged(e) { + if (!e.detail.displayName) return; + this._changePeerDisplayName(e.detail.peerId, e.detail.displayName); + } + + _onKeyDown(e) { + if (document.querySelectorAll('x-dialog[show]').length === 0 && window.pasteMode.activated && e.code === "Escape") { + Events.fire('deactivate-paste-mode'); + } + } + + _onPeerJoined(msg) { + this._joinPeer(msg.peer, msg.roomType, msg.roomSecret); + } + + _joinPeer(peer, roomType, roomSecret) { + const existingPeer = this.peers[peer.id]; + if (existingPeer) { + // peer already exists. Abort but add roomType to GUI and update roomSecret + // skip if peer is a tab on the same browser + if (!existingPeer.sameBrowser()) { + // add roomType to PeerUI + if (!existingPeer.roomTypes.includes(roomType)) { + existingPeer.roomTypes.push(roomType); + } + this._redrawPeerRoomTypes(peer.id); + + if (roomType === "secret") existingPeer.roomSecret = roomSecret; + } + return; + } + peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id); + + if (!(roomType === "secret" && peer.sameBrowser())) { + peer.roomTypes = [roomType]; + peer.roomSecret = roomSecret; + } + + this.peers[peer.id] = peer; + } + + _onPeerConnected(peerId, connectionHash) { + if (!this.peers[peerId] || $(peerId)) return; + + const peer = this.peers[peerId]; + + new PeerUI(peer, connectionHash); + } + + _redrawPeerRoomTypes(peerId) { + const peer = this.peers[peerId] + const peerNode = $(peerId); + if (!peerNode) return; + peerNode.classList.remove('type-ip', 'type-secret'); + if (!peer.sameBrowser()) { + peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`)); + } + } + + evaluateOverflowing() { + if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) { + this.$xPeers.classList.add('overflowing'); + } else { + this.$xPeers.classList.remove('overflowing'); + } + } + + _onPeers(msg) { + msg.peers.forEach(peer => this._joinPeer(peer, msg.roomType, msg.roomSecret)); + } + + _onPeerDisconnected(peerId) { + const $peer = $(peerId); + if (!$peer) return; + $peer.remove(); + this.evaluateOverflowing(); + } + + _onSecretRoomDeleted(roomSecret) { + for (const peerId in this.peers) { + const peer = this.peers[peerId]; + if (peer.roomSecret === roomSecret) { + let index = peer.roomTypes.indexOf('secret'); + peer.roomTypes.splice(index, 1); + peer.roomSecret = ""; + + if (peer.roomTypes.length) { + this._redrawPeerRoomTypes(peerId) + return; + } + + this._onPeerDisconnected(peerId); + return; + } + } + } + + _onSetProgress(progress) { + const $peer = $(progress.peerId); + if (!$peer) return; + $peer.ui.setProgress(progress.progress, progress.status) + } + + _onDrop(e) { + e.preventDefault(); + if (!$$('x-peer') || !$$('x-peer').contains(e.target)) { + this._activatePasteMode(e.dataTransfer.files, '') + } + this._onDragEnd(); + } + + _onDragOver(e) { + e.preventDefault(); + this.$xInstructions.setAttribute('drop-bg', 1); + this.$xNoPeers.setAttribute('drop-bg', 1); + } + + _onDragEnd() { + this.$xInstructions.removeAttribute('drop-bg', 1); + this.$xNoPeers.removeAttribute('drop-bg'); + } + + _onPaste(e) { + if(document.querySelectorAll('x-dialog[show]').length === 0) { + // prevent send on paste when dialog is open + e.preventDefault() + const files = e.clipboardData.files; + const text = e.clipboardData.getData("Text"); + if (files.length === 0 && text.length === 0) return; + this._activatePasteMode(files, text); + } + } + + _activatePasteMode(files, text) { + if (!window.pasteMode.activated && (files.length > 0 || text.length > 0)) { + let descriptor; + let noPeersMessage; + + if (files.length === 1) { + descriptor = files[0].name; + noPeersMessage = `Open MoreLoad on other devices to send
${descriptor}`; + } else if (files.length > 1) { + descriptor = `${files[0].name} and ${files.length-1} other files`; + noPeersMessage = `Open MoreLoad on other devices to send
${descriptor}`; + } else { + descriptor = "shared text"; + noPeersMessage = `Open MoreLoad on other devices to send
${descriptor}`; + } + + this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + this.$xInstructions.querySelector('p').style.display = 'block'; + this.$xInstructions.setAttribute('desktop', `Click to send`); + this.$xInstructions.setAttribute('mobile', `Tap to send`); + + this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; + + const _callback = (e) => this._sendClipboardData(e, files, text); + Events.on('paste-pointerdown', _callback); + Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true }); + + this.$cancelPasteModeBtn.removeAttribute('hidden'); + + window.pasteMode.descriptor = descriptor; + window.pasteMode.activated = true; + + console.log('Paste mode activated.'); + Events.fire('paste-mode-changed'); + } + } + + _cancelPasteMode() { + Events.fire('deactivate-paste-mode'); + } + + _deactivatePasteMode(_callback) { + if (window.pasteMode.activated) { + window.pasteMode.descriptor = undefined; + window.pasteMode.activated = false; + Events.off('paste-pointerdown', _callback); + + this.$xInstructions.querySelector('p').innerText = ''; + this.$xInstructions.querySelector('p').style.display = 'none'; + + this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); + this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); + + this.$xNoPeers.querySelector('h2').innerHTML = 'Open MoreLoad on other devices to send files'; + + this.$cancelPasteModeBtn.setAttribute('hidden', ""); + + console.log('Paste mode deactivated.') + Events.fire('paste-mode-changed'); + } + } + + _sendClipboardData(e, files, text) { + // send the pasted file/text content + const peerId = e.detail.peerId; + + if (files.length > 0) { + Events.fire('files-selected', { + files: files, + to: peerId + }); + } else if (text.length > 0) { + Events.fire('send-text', { + text: text, + to: peerId + }); + } + } +} + +class PeerUI { + + constructor(peer, connectionHash) { + this._peer = peer; + this._connectionHash = + `${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`; + this._initDom(); + this._bindListeners(); + + $$('x-peers').appendChild(this.$el) + Events.fire('peer-added'); + this.$xInstructions = $$('x-instructions'); + } + + html() { + let title; + let input = ''; + if (window.pasteMode.activated) { + title = `Click to send ${window.pasteMode.descriptor}`; + } else { + title = 'Click to send files or right click to send a message'; + input = ''; + } + this.$el.innerHTML = ` + `; + + this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); + this.$el.querySelector('.name').textContent = this._displayName(); + this.$el.querySelector('.device-name').textContent = this._deviceName(); + this.$el.querySelector('.connection-hash').textContent = this._connectionHash; + } + + _initDom() { + this.$el = document.createElement('x-peer'); + this.$el.id = this._peer.id; + this.$el.ui = this; + this.$el.classList.add('center'); + if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`)); + if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer'); + this.html(); + + this._callbackInput = e => this._onFilesSelected(e) + this._callbackClickSleep = _ => NoSleepUI.enable() + this._callbackTouchStartSleep = _ => NoSleepUI.enable() + this._callbackDrop = e => this._onDrop(e) + this._callbackDragEnd = e => this._onDragEnd(e) + this._callbackDragLeave = e => this._onDragEnd(e) + this._callbackDragOver = e => this._onDragOver(e) + this._callbackContextMenu = e => this._onRightClick(e) + this._callbackTouchStart = e => this._onTouchStart(e) + this._callbackTouchEnd = e => this._onTouchEnd(e) + this._callbackPointerDown = e => this._onPointerDown(e) + // PasteMode + Events.on('paste-mode-changed', _ => this._onPasteModeChanged()); + } + + _onPasteModeChanged() { + this.html(); + this._bindListeners(); + } + + _bindListeners() { + if(!window.pasteMode.activated) { + // Remove Events Paste Mode + this.$el.removeEventListener('pointerdown', this._callbackPointerDown); + + // Add Events Normal Mode + this.$el.querySelector('input').addEventListener('change', this._callbackInput); + this.$el.addEventListener('click', this._callbackClickSleep); + this.$el.addEventListener('touchstart', this._callbackTouchStartSleep); + this.$el.addEventListener('drop', this._callbackDrop); + this.$el.addEventListener('dragend', this._callbackDragEnd); + this.$el.addEventListener('dragleave', this._callbackDragLeave); + this.$el.addEventListener('dragover', this._callbackDragOver); + this.$el.addEventListener('contextmenu', this._callbackContextMenu); + this.$el.addEventListener('touchstart', this._callbackTouchStart); + this.$el.addEventListener('touchend', this._callbackTouchEnd); + } else { + // Remove Events Normal Mode + this.$el.removeEventListener('click', this._callbackClickSleep); + this.$el.removeEventListener('touchstart', this._callbackTouchStartSleep); + this.$el.removeEventListener('drop', this._callbackDrop); + this.$el.removeEventListener('dragend', this._callbackDragEnd); + this.$el.removeEventListener('dragleave', this._callbackDragLeave); + this.$el.removeEventListener('dragover', this._callbackDragOver); + this.$el.removeEventListener('contextmenu', this._callbackContextMenu); + this.$el.removeEventListener('touchstart', this._callbackTouchStart); + this.$el.removeEventListener('touchend', this._callbackTouchEnd); + + // Add Events Paste Mode + this.$el.addEventListener('pointerdown', this._callbackPointerDown); + } + } + + _onPointerDown(e) { + // Prevents triggering of event twice on touch devices + e.stopPropagation(); + e.preventDefault(); + Events.fire('paste-pointerdown', { + peerId: this._peer.id + }); + } + + _displayName() { + return this._peer.name.displayName; + } + + _deviceName() { + return this._peer.name.deviceName; + } + + _icon() { + const device = this._peer.name.device || this._peer.name; + if (device.type === 'mobile') { + return '#phone-iphone'; + } + if (device.type === 'tablet') { + return '#tablet-mac'; + } + return '#desktop-mac'; + } + + _onFilesSelected(e) { + const $input = e.target; + const files = $input.files; + Events.fire('files-selected', { + files: files, + to: this._peer.id + }); + $input.files = null; // reset input + } + + setProgress(progress, status) { + const $progress = this.$el.querySelector('.progress'); + if (0.5 < progress && progress < 1) { + $progress.classList.add('over50'); + } else { + $progress.classList.remove('over50'); + } + if (progress < 1) { + this.$el.setAttribute('status', status); + } else { + this.$el.removeAttribute('status'); + progress = 0; + } + const degrees = `rotate(${360 * progress}deg)`; + $progress.style.setProperty('--progress', degrees); + } + + _onDrop(e) { + e.preventDefault(); + Events.fire('files-selected', { + files: e.dataTransfer.files, + to: this._peer.id + }); + this._onDragEnd(); + } + + _onDragOver() { + this.$el.setAttribute('drop', 1); + this.$xInstructions.setAttribute('drop-peer', 1); + } + + _onDragEnd() { + this.$el.removeAttribute('drop'); + this.$xInstructions.removeAttribute('drop-peer', 1); + } + + _onRightClick(e) { + e.preventDefault(); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); + } + + _onTouchStart(e) { + this._touchStart = Date.now(); + this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610); + } + + _onTouchEnd(e) { + if (Date.now() - this._touchStart < 500) { + clearTimeout(this._touchTimer); + } else if (this._touchTimer) { // this was a long tap + e.preventDefault(); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); + } + this._touchTimer = null; + } +} + +class Dialog { + constructor(id) { + this.$el = $(id); + this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', _ => this.hide())); + this.$autoFocus = this.$el.querySelector('[autofocus]'); + Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); + } + + show() { + this.$el.setAttribute('show', 1); + if (this.$autoFocus) this.$autoFocus.focus(); + } + + isShown() { + return !!this.$el.attributes["show"]; + } + + hide() { + this.$el.removeAttribute('show'); + if (this.$autoFocus) { + document.activeElement.blur(); + window.blur(); + } + document.title = 'MoreLoad'; + document.changeFavicon("images/favicon-96x96.png"); + this.correspondingPeerId = undefined; + } + + _onPeerDisconnected(peerId) { + if (this.isShown() && this.correspondingPeerId === peerId) { + this.hide(); + Events.fire('notify-user', 'Selected peer left.') + } + } +} + +class ReceiveDialog extends Dialog { + constructor(id) { + super(id); + this.$fileDescription = this.$el.querySelector('.file-description'); + this.$displayName = this.$el.querySelector('.display-name'); + this.$fileStem = this.$el.querySelector('.file-stem'); + this.$fileExtension = this.$el.querySelector('.file-extension'); + this.$fileOther = this.$el.querySelector('.file-other'); + this.$fileSize = this.$el.querySelector('.file-size'); + this.$previewBox = this.$el.querySelector('.file-preview'); + this.$receiveTitle = this.$el.querySelector('h2:first-of-type'); + } + + _formatFileSize(bytes) { + // 1 GB = 1024 MB = 1024^2 KB = 1024^3 B + // 1024^2 = 104876; 1024^3 = 1073741824 + if (bytes >= 1073741824) { + return Math.round(10 * bytes / 1073741824) / 10 + ' GB'; + } else if (bytes >= 1048576) { + return Math.round(bytes / 1048576) + ' MB'; + } else if (bytes > 1024) { + return Math.round(bytes / 1024) + ' KB'; + } else { + return bytes + ' Bytes'; + } + } + + _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { + if (files.length > 1) { + let fileOtherText = ` and ${files.length - 1} other `; + if (files.length === 2) { + fileOtherText += imagesOnly ? 'image' : 'file'; + } else { + fileOtherText += imagesOnly ? 'images' : 'files'; + } + this.$fileOther.innerText = fileOtherText; + } + + const fileName = files[0].name; + const fileNameSplit = fileName.split('.'); + const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; + this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); + this.$fileExtension.innerText = fileExtension; + this.$displayName.innerText = displayName; + this.$displayName.title = connectionHash; + this.$fileSize.innerText = this._formatFileSize(totalSize); + } +} + +class ReceiveFileDialog extends ReceiveDialog { + + constructor() { + super('receive-file-dialog'); + + this.$downloadBtn = this.$el.querySelector('#download-btn'); + this.$shareBtn = this.$el.querySelector('#share-btn'); + + Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize)); + this._filesQueue = []; + } + + _onFilesReceived(sender, files, imagesOnly, totalSize) { + const displayName = $(sender).ui._displayName(); + const connectionHash = $(sender).ui._connectionHash; + this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); + this._nextFiles(); + window.blop.play(); + } + + _nextFiles() { + if (this._busy) return; + this._busy = true; + const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift(); + this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize); + } + + _dequeueFile() { + if (!this._filesQueue.length) { // nothing to do + this._busy = false; + return; + } + // dequeue next file + setTimeout(_ => { + this._busy = false; + this._nextFiles(); + }, 300); + } + + createPreviewElement(file) { + return new Promise((resolve, reject) => { + try { + let mime = file.type.split('/')[0] + let previewElement = { + image: 'img', + audio: 'audio', + video: 'video' + } + + if (Object.keys(previewElement).indexOf(mime) === -1) { + resolve(false); + } else { + let element = document.createElement(previewElement[mime]); + element.controls = true; + element.onload = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onloadeddata = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onerror = _ => { + reject(`${mime} preview could not be loaded from type ${file.type}`); + }; + element.src = URL.createObjectURL(file); + } + } catch (e) { + reject(`preview could not be loaded from type ${file.type}`); + } + }); + } + + async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) { + this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize); + + let descriptor, url, filenameDownload; + if (files.length === 1) { + descriptor = imagesOnly ? 'Image' : 'File'; + } else { + descriptor = imagesOnly ? 'Images' : 'Files'; + } + this.$receiveTitle.innerText = `${descriptor} Received`; + + const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); + if (canShare) { + this.$shareBtn.removeAttribute('hidden'); + this.$shareBtn.onclick = _ => { + navigator.share({files: files}) + .catch(err => { + console.error(err); + }); + } + } + + let downloadZipped = false; + if (files.length > 1) { + downloadZipped = true; + try { + let bytesCompleted = 0; + zipper.createNewZipWriter(); + for (let i=0; i { + Events.fire('set-progress', { + peerId: peerId, + progress: (bytesCompleted + progress) / totalSize, + status: 'process' + }) + } + }); + bytesCompleted += files[i].size; + } + url = await zipper.getBlobURL(); + + let now = new Date(Date.now()); + let year = now.getFullYear().toString(); + let month = (now.getMonth()+1).toString(); + month = month.length < 2 ? "0" + month : month; + let date = now.getDate().toString(); + date = date.length < 2 ? "0" + date : date; + let hours = now.getHours().toString(); + hours = hours.length < 2 ? "0" + hours : hours; + let minutes = now.getMinutes().toString(); + minutes = minutes.length < 2 ? "0" + minutes : minutes; + filenameDownload = `MoreLoad_files_${year+month+date}_${hours+minutes}.zip`; + } catch (e) { + console.error(e); + downloadZipped = false; + } + } + + this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.onclick = _ => { + if (downloadZipped) { + let tmpZipBtn = document.createElement("a"); + tmpZipBtn.download = filenameDownload; + tmpZipBtn.href = url; + tmpZipBtn.click(); + } else { + this._downloadFilesIndividually(files); + } + + if (!canShare) { + this.$downloadBtn.innerText = "Download again"; + } + Events.fire('notify-user', `${descriptor} downloaded successfully`); + this.$downloadBtn.style.pointerEvents = "none"; + setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); + }; + + document.title = files.length === 1 + ? 'File received - MoreLoad' + : `${files.length} Files received - MoreLoad`; + document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) + this.show(); + + setTimeout(_ => { + if (canShare) { + this.$shareBtn.click(); + } else { + this.$downloadBtn.click(); + } + }, 500); + + this.createPreviewElement(files[0]) + .then(canPreview => { + if (canPreview) { + console.log('the file is able to preview'); + } else { + console.log('the file is not able to preview'); + } + }) + .catch(r => console.error(r)); + } + + _downloadFilesIndividually(files) { + let tmpBtn = document.createElement("a"); + for (let i=0; i this._respondToFileTransferRequest(true)); + this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false)); + + Events.on('files-transfer-request', e => this._onRequestFileTransfer(e.detail.request, e.detail.peerId)) + Events.on('keydown', e => this._onKeyDown(e)); + this._filesTransferRequestQueue = []; + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this._respondToFileTransferRequest(false); + } + } + + _onRequestFileTransfer(request, peerId) { + this._filesTransferRequestQueue.push({request: request, peerId: peerId}); + if (this.isShown()) return; + this._dequeueRequests(); + } + + _dequeueRequests() { + if (!this._filesTransferRequestQueue.length) return; + let {request, peerId} = this._filesTransferRequestQueue.shift(); + this._showRequestDialog(request, peerId) + } + + _showRequestDialog(request, peerId) { + this.correspondingPeerId = peerId; + + const displayName = $(peerId).ui._displayName(); + const connectionHash = $(peerId).ui._connectionHash; + this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize); + + if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") { + let element = document.createElement('img'); + element.src = request.thumbnailDataUrl; + this.$previewBox.appendChild(element) + } + + this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` + + document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - MoreLoad`; + document.changeFavicon("images/favicon-96x96-notification.png"); + this.show(); + } + + _respondToFileTransferRequest(accepted) { + Events.fire('respond-to-files-transfer-request', { + to: this.correspondingPeerId, + accepted: accepted + }) + if (accepted) { + Events.fire('set-progress', {peerId: this.correspondingPeerId, progress: 0, status: 'wait'}); + NoSleepUI.enable(); + } + this.hide(); + } + + hide() { + // clear previewBox after dialog is closed + setTimeout(_ => this.$previewBox.innerHTML = '', 300); + + super.hide(); + + // show next request + setTimeout(_ => this._dequeueRequests(), 500); + } +} + +class PairDeviceDialog extends Dialog { + constructor() { + super('pair-device-dialog'); + this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input'); + this.$submitBtn = this.$el.querySelector('button[type="submit"]'); + this.$roomKey = this.$el.querySelector('#room-key'); + this.$qrCode = this.$el.querySelector('#room-key-qr-code'); + this.$pairDeviceBtn = $('pair-device'); + this.$editPairedDevicesBtn = $('edit-paired-devices'); + this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); + this.$createJoinForm = this.$el.querySelector('form'); + + this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e)); + this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate()); + + this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel()) + this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); + this.$inputRoomKeyChars.forEach(el => el.addEventListener('keydown', e => this._onCharsKeyDown(e))); + this.$inputRoomKeyChars.forEach(el => el.addEventListener('focus', e => e.target.select())); + this.$inputRoomKeyChars.forEach(el => el.addEventListener('click', e => e.target.select())); + + Events.on('keydown', e => this._onKeyDown(e)); + Events.on('ws-connected', _ => this._onWsConnected()); + Events.on('ws-disconnected', _ => this.hide()); + Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail)); + Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret)); + Events.on('peers', e => this._onPeers(e.detail)); + Events.on('peer-joined', e => this._onPeerJoined(e.detail)); + Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid()); + Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail)); + Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets()) + Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + this.$el.addEventListener('paste', e => this._onPaste(e)); + + this.evaluateRoomKeyChars(); + this.evaluateUrlAttributes(); + + this.pairPeer = {}; + } + + _onCharsInput(e) { + e.target.value = e.target.value.replace(/\D/g,''); + if (!e.target.value) return; + this.evaluateRoomKeyChars(); + + let nextSibling = e.target.nextElementSibling; + if (nextSibling) { + e.preventDefault(); + nextSibling.focus(); + } + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + // Timeout to prevent paste mode from getting cancelled simultaneously + setTimeout(_ => this._pairDeviceCancel(), 50); + } + } + + _onCharsKeyDown(e) { + let previousSibling = e.target.previousElementSibling; + let nextSibling = e.target.nextElementSibling; + if (e.key === "Backspace" && previousSibling && !e.target.value) { + previousSibling.value = ''; + previousSibling.focus(); + } else if (e.key === "ArrowRight" && nextSibling) { + e.preventDefault(); + nextSibling.focus(); + } else if (e.key === "ArrowLeft" && previousSibling) { + e.preventDefault(); + previousSibling.focus(); + } + } + + _onPaste(e) { + e.preventDefault(); + let num = e.clipboardData.getData("Text").replace(/\D/g,'').substring(0, 6); + for (let i = 0; i < num.length; i++) { + document.activeElement.value = num.charAt(i); + let nextSibling = document.activeElement.nextElementSibling; + if (!nextSibling) break; + nextSibling.focus(); + } + this.evaluateRoomKeyChars(); + } + + evaluateRoomKeyChars() { + if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) { + this.$submitBtn.setAttribute("disabled", ""); + } else { + this.inputRoomKey = ""; + this.$inputRoomKeyChars.forEach(el => { + this.inputRoomKey += el.value; + }) + this.$submitBtn.removeAttribute("disabled"); + if (document.activeElement === this.$inputRoomKeyChars[5]) { + this._pairDeviceJoin(this.inputRoomKey); + } + } + } + + evaluateUrlAttributes() { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('room_key')) { + this._pairDeviceJoin(urlParams.get('room_key')); + window.history.replaceState({}, "title**", '/'); //remove room_key from url + } + } + + _onWsConnected() { + this.$pairDeviceBtn.removeAttribute('hidden'); + this._evaluateNumberRoomSecrets(); + } + + _pairDeviceInitiate() { + Events.fire('pair-device-initiate'); + } + + _pairDeviceInitiated(msg) { + this.roomKey = msg.roomKey; + this.roomSecret = msg.roomSecret; + this.$roomKey.innerText = `${this.roomKey.substring(0,3)} ${this.roomKey.substring(3,6)}` + // Display the QR code for the url + const qr = new QRCode({ + content: this._getShareRoomURL(), + width: 150, + height: 150, + padding: 0, + background: "transparent", + color: `rgb(var(--text-color))`, + ecl: "L", + join: true + }); + this.$qrCode.innerHTML = qr.svg(); + this.$inputRoomKeyChars.forEach(el => el.removeAttribute("disabled")); + this.show(); + } + + _getShareRoomURL() { + let url = new URL(location.href); + url.searchParams.append('room_key', this.roomKey) + return url.href; + } + + _onSubmit(e) { + e.preventDefault(); + this._pairDeviceJoin(this.inputRoomKey); + } + + _pairDeviceJoin(roomKey) { + if (/^\d{6}$/g.test(roomKey)) { + roomKey = roomKey.substring(0,6); + Events.fire('pair-device-join', roomKey); + let lastChar = this.$inputRoomKeyChars[5]; + lastChar.focus(); + } + } + + _pairDeviceJoined(peerId, roomSecret) { + // skip adding to IndexedDB if peer is another tab on the same browser + if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { + this._cleanUp(); + this.hide(); + Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); + return; + } + + // save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret + this.pairPeer = { + "peerId": peerId, + "roomSecret": roomSecret + }; + } + + _onPeers(message) { + if (!Object.keys(this.pairPeer)) return; + + message.peers.forEach(messagePeer => { + this._evaluateJoinedPeer(messagePeer.id, message.roomSecret); + }); + } + + _onPeerJoined(message) { + if (!Object.keys(this.pairPeer)) return; + + this._evaluateJoinedPeer(message.peer.id, message.roomSecret); + } + + _evaluateJoinedPeer(peerId, roomSecret) { + const samePeerId = peerId === this.pairPeer.peerId; + const sameRoomSecret = roomSecret === this.pairPeer.roomSecret; + + if (!peerId || !roomSecret || !samePeerId || !sameRoomSecret) return; + + this._onPairPeerJoined(peerId, roomSecret); + this.pairPeer = {}; + } + + _onPairPeerJoined(peerId, roomSecret) { + // if devices are paired that are already connected we must save the names at this point + const $peer = $(peerId); + let displayName, deviceName; + if ($peer) { + displayName = $peer.ui._peer.name.displayName; + deviceName = $peer.ui._peer.name.deviceName; + } + + PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) + .then(_ => { + Events.fire('notify-user', 'Devices paired successfully.'); + this._evaluateNumberRoomSecrets(); + }) + .finally(_ => { + this._cleanUp(); + this.hide(); + }) + .catch(_ => { + Events.fire('notify-user', 'Paired devices are not persistent.'); + PersistentStorage.logBrowserNotCapable(); + }); + } + + _pairDeviceJoinKeyInvalid() { + Events.fire('notify-user', 'Key not valid'); + } + + _pairDeviceCancel() { + this.hide(); + this._cleanUp(); + Events.fire('pair-device-cancel'); + } + + _pairDeviceCanceled(roomKey) { + Events.fire('notify-user', `Key ${roomKey} invalidated.`); + } + + _cleanUp() { + this.roomSecret = null; + this.roomKey = null; + this.inputRoomKey = ''; + this.$inputRoomKeyChars.forEach(el => el.value = ''); + this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", "")); + this.pairPeer = {}; + } + + _onSecretRoomDeleted(roomSecret) { + PersistentStorage.deleteRoomSecret(roomSecret).then(_ => { + this._evaluateNumberRoomSecrets(); + }); + } + + _evaluateNumberRoomSecrets() { + PersistentStorage.getAllRoomSecrets().then(roomSecrets => { + if (roomSecrets.length > 0) { + this.$editPairedDevicesBtn.removeAttribute('hidden'); + this.$footerInstructionsPairedDevices.removeAttribute('hidden'); + } else { + this.$editPairedDevicesBtn.setAttribute('hidden', ''); + this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); + } + Events.fire('bg-resize'); + }); + } +} + +class EditPairedDevicesDialog extends Dialog { + constructor() { + super('edit-paired-devices-dialog'); + this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper'); + $('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices()); + + Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e)); + Events.on('keydown', e => this._onKeyDown(e)); + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this.hide(); + } + } + + async _initDOM() { + const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries(); + roomSecretsEntries.forEach(roomSecretsEntry => { + let $pairedDevice = document.createElement('div'); + $pairedDevice.classList = ["paired-device"]; + + $pairedDevice.innerHTML = ` +
+ ${roomSecretsEntry.display_name} +
+
+ ${roomSecretsEntry.device_name} +
+
+ + +
` + + $pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => { + PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => { + Events.fire('auto-accept-updated', { + 'roomSecret': roomSecretsEntry.entry.secret, + 'autoAccept': e.target.checked + }); + }); + }); + + $pairedDevice.querySelector('button').addEventListener('click', e => { + PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => { + Events.fire('room-secrets-deleted', [roomSecret]); + Events.fire('evaluate-number-room-secrets'); + e.target.parentNode.parentNode.remove(); + }); + }) + + this.$pairedDevicesWrapper.appendChild($pairedDevice) + }) + + } + + hide() { + super.hide(); + setTimeout(_ => { + this.$pairedDevicesWrapper.innerHTML = "" + }, 300); + } + + _onEditPairedDevices() { + this._initDOM().then(_ => this.show()); + } + + _clearRoomSecrets() { + PersistentStorage.getAllRoomSecrets() + .then(roomSecrets => { + PersistentStorage.clearRoomSecrets().finally(_ => { + Events.fire('room-secrets-deleted', roomSecrets); + Events.fire('evaluate-number-room-secrets'); + Events.fire('notify-user', 'All Devices unpaired.'); + this.hide(); + }) + }); + } + + _onPeerDisplayNameChanged(e) { + const peerId = e.detail.peerId; + const peerNode = $(peerId); + + if (!peerNode) return; + + const peer = peerNode.ui._peer; + + if (!peer.roomSecret) return; + + PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => { + console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`); + }) + } +} + +class SendTextDialog extends Dialog { + constructor() { + super('send-text-dialog'); + Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); + this.$text = this.$el.querySelector('#text-input'); + this.$peerDisplayName = this.$el.querySelector('.display-name'); + this.$form = this.$el.querySelector('form'); + this.$submit = this.$el.querySelector('button[type="submit"]'); + this.$form.addEventListener('submit', e => this._onSubmit(e)); + this.$text.addEventListener('input', e => this._onChange(e)); + Events.on("keydown", e => this._onKeyDown(e)); + } + + async _onKeyDown(e) { + if (this.isShown()) { + if (e.code === "Escape") { + this.hide(); + } else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) { + if (this._textInputEmpty()) return; + this._send(); + } + } + } + + _textInputEmpty() { + return !this.$text.innerText || this.$text.innerText === "\n"; + } + + _onChange(e) { + if (this._textInputEmpty()) { + this.$submit.setAttribute('disabled', ''); + } else { + this.$submit.removeAttribute('disabled'); + } + } + + _onRecipient(peerId, deviceName) { + this.correspondingPeerId = peerId; + this.$peerDisplayName.innerText = deviceName; + this.show(); + + const range = document.createRange(); + const sel = window.getSelection(); + + this.$text.focus(); + range.selectNodeContents(this.$text); + sel.removeAllRanges(); + sel.addRange(range); + } + + _onSubmit(e) { + e.preventDefault(); + this._send(); + } + + _send() { + Events.fire('send-text', { + to: this.correspondingPeerId, + text: this.$text.innerText + }); + this.$text.value = ""; + this.hide(); + } +} + +class ReceiveTextDialog extends Dialog { + constructor() { + super('receive-text-dialog'); + Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId)); + this.$text = this.$el.querySelector('#text'); + this.$copy = this.$el.querySelector('#copy'); + this.$close = this.$el.querySelector('#close'); + + this.$copy.addEventListener('click', _ => this._onCopy()); + this.$close.addEventListener('click', _ => this.hide()); + + Events.on("keydown", e => this._onKeyDown(e)); + + this.$displayNameNode = this.$el.querySelector('.display-name'); + this._receiveTextQueue = []; + } + + async _onKeyDown(e) { + if (this.isShown()) { + if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) { + await this._onCopy() + this.hide(); + } else if (e.code === "Escape") { + this.hide(); + } + } + } + + _onText(text, peerId) { + window.blop.play(); + this._receiveTextQueue.push({text: text, peerId: peerId}); + this._setDocumentTitleMessages(); + if (this.isShown()) return; + this._dequeueRequests(); + } + + _dequeueRequests() { + if (!this._receiveTextQueue.length) return; + let {text, peerId} = this._receiveTextQueue.shift(); + this._showReceiveTextDialog(text, peerId); + } + + _showReceiveTextDialog(text, peerId) { + this.$displayNameNode.innerText = $(peerId).ui._displayName(); + + this.$text.innerText = text; + this.$text.classList.remove('text-center'); + + // Beautify text if text is short + if (text.length < 2000) { + // replace urls with actual links + this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => { + return `${url}`; + }); + + if (!/\s/.test(text)) { + this.$text.classList.add('text-center'); + } + } + + this._setDocumentTitleMessages(); + + document.changeFavicon("images/favicon-96x96-notification.png"); + this.show(); + } + + _setDocumentTitleMessages() { + document.title = !this._receiveTextQueue.length + ? 'Message Received - MoreLoad' + : `${this._receiveTextQueue.length + 1} Messages Received - MoreLoad`; + } + + async _onCopy() { + await navigator.clipboard.writeText(this.$text.textContent); + Events.fire('notify-user', 'Copied to clipboard'); + this.hide(); + } + + hide() { + super.hide(); + setTimeout(_ => this._dequeueRequests(), 500); + } +} + +class Base64ZipDialog extends Dialog { + + constructor() { + super('base64-paste-dialog'); + const urlParams = new URL(window.location).searchParams; + const base64Text = urlParams.get('base64text'); + const base64Zip = urlParams.get('base64zip'); + const base64Hash = window.location.hash.substring(1); + + this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); + this.$fallbackTextarea = this.$el.querySelector('.textarea'); + + if (base64Text) { + this.show(); + if (base64Text === "paste") { + // ?base64text=paste + // base64 encoded string is ready to be pasted from clipboard + this.preparePasting("text"); + } else if (base64Text === "hash") { + // ?base64text=hash#BASE64ENCODED + // base64 encoded string is url hash which is never sent to server and faster (recommended) + this.processBase64Text(base64Hash) + .catch(_ => { + Events.fire('notify-user', 'Text content is incorrect.'); + console.log("Text content incorrect."); + }).finally(_ => { + this.hide(); + }); + } else { + // ?base64text=BASE64ENCODED + // base64 encoded string was part of url param (not recommended) + this.processBase64Text(base64Text) + .catch(_ => { + Events.fire('notify-user', 'Text content is incorrect.'); + console.log("Text content incorrect."); + }).finally(_ => { + this.hide(); + }); + } + } else if (base64Zip) { + this.show(); + if (base64Zip === "hash") { + // ?base64zip=hash#BASE64ENCODED + // base64 encoded zip file is url hash which is never sent to the server + this.processBase64Zip(base64Hash) + .catch(_ => { + Events.fire('notify-user', 'File content is incorrect.'); + console.log("File content incorrect."); + }).finally(_ => { + this.hide(); + }); + } else { + // ?base64zip=paste || ?base64zip=true + this.preparePasting('files'); + } + } + } + + _setPasteBtnToProcessing() { + this.$pasteBtn.style.pointerEvents = "none"; + this.$pasteBtn.innerText = "Processing..."; + } + + preparePasting(type) { + if (navigator.clipboard.readText) { + this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this._clickCallback = _ => this.processClipboard(type); + this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); + } else { + console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") + this.$pasteBtn.setAttribute('hidden', ''); + this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.removeAttribute('hidden'); + this._inputCallback = _ => this.processInput(type); + this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); + this.$fallbackTextarea.focus(); + } + } + + async processInput(type) { + const base64 = this.$fallbackTextarea.textContent; + this.$fallbackTextarea.textContent = ''; + await this.processBase64(type, base64); + } + + async processClipboard(type) { + const base64 = await navigator.clipboard.readText(); + await this.processBase64(type, base64); + } + + isValidBase64(base64) { + try { + // check if input is base64 encoded + window.atob(base64); + return true; + } catch (e) { + // input is not base64 string. + return false; + } + } + + async processBase64(type, base64) { + if (!base64 || !this.isValidBase64(base64)) return; + this._setPasteBtnToProcessing(); + try { + if (type === "text") { + await this.processBase64Text(base64); + } else { + await this.processBase64Zip(base64); + } + } catch(_) { + Events.fire('notify-user', 'Clipboard content is incorrect.'); + console.log("Clipboard content is incorrect.") + } + this.hide(); + } + + processBase64Text(base64Text){ + return new Promise((resolve) => { + this._setPasteBtnToProcessing(); + let decodedText = decodeURIComponent(escape(window.atob(base64Text))); + Events.fire('activate-paste-mode', {files: [], text: decodedText}); + resolve(); + }); + } + + async processBase64Zip(base64zip) { + this._setPasteBtnToProcessing(); + let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + const zipBlob = new File([u8arr], 'archive.zip'); + + let files = []; + const zipEntries = await zipper.getEntries(zipBlob); + for (let i = 0; i < zipEntries.length; i++) { + let fileBlob = await zipper.getData(zipEntries[i]); + files.push(new File([fileBlob], zipEntries[i].filename)); + } + Events.fire('activate-paste-mode', {files: files, text: ""}); + } + + clearBrowserHistory() { + window.history.replaceState({}, "Rewrite URL", '/'); + } + + hide() { + this.clearBrowserHistory(); + this.$pasteBtn.removeEventListener('click', _ => this._clickCallback()); + this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback()); + super.hide(); + } +} + +class Toast extends Dialog { + constructor() { + super('toast'); + Events.on('notify-user', e => this._onNotify(e.detail)); + } + + _onNotify(message) { + if (this.hideTimeout) clearTimeout(this.hideTimeout); + this.$el.textContent = message; + this.show(); + this.hideTimeout = setTimeout(_ => this.hide(), 5000); + } +} + +class Notifications { + + constructor() { + // Check if the browser supports notifications + if (!('Notification' in window)) return; + + // Check whether notification permissions have already been granted + if (Notification.permission !== 'granted') { + this.$button = $('notification'); + this.$button.removeAttribute('hidden'); + this.$button.addEventListener('click', _ => this._requestPermission()); + } + Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); + Events.on('files-received', e => this._downloadNotification(e.detail.files)); + Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId)); + } + + _requestPermission() { + Notification.requestPermission(permission => { + if (permission !== 'granted') { + Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); + return; + } + Events.fire('notify-user', 'Notifications enabled.'); + this.$button.setAttribute('hidden', 1); + }); + } + + _notify(title, body) { + const config = { + body: body, + icon: '/images/logo_transparent_128x128.png', + } + let notification; + try { + notification = new Notification(title, config); + } catch (e) { + // Android doesn't support "new Notification" if service worker is installed + if (!serviceWorker || !serviceWorker.showNotification) return; + notification = serviceWorker.showNotification(title, config); + } + + // Notification is persistent on Android. We have to close it manually + const visibilitychangeHandler = () => { + if (document.visibilityState === 'visible') { + notification.close(); + Events.off('visibilitychange', visibilitychangeHandler); + } + }; + Events.on('visibilitychange', visibilitychangeHandler); + + return notification; + } + + _messageNotification(message, peerId) { + if (document.visibilityState !== 'visible') { + const peerDisplayName = $(peerId).ui._displayName(); + if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { + const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); + this._bind(notification, _ => window.open(message, '_blank', null, true)); + } else { + const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); + this._bind(notification, _ => this._copyText(message, notification)); + } + } + } + + _downloadNotification(files) { + if (document.visibilityState !== 'visible') { + let imagesOnly = true; + for(let i=0; i= 2) { + title += ` and ${files.length - 1} other `; + title += imagesOnly ? 'image' : 'file'; + if (files.length > 2) title += "s"; + } + const notification = this._notify(title, 'Click to download'); + this._bind(notification, _ => this._download(notification)); + } + } + + _requestNotification(request, peerId) { + if (document.visibilityState !== 'visible') { + let imagesOnly = true; + for(let i=0; i 1) { + descriptor = imagesOnly ? ' images' : ' files'; + } else { + descriptor = imagesOnly ? ' image' : ' file'; + } + let displayName = $(peerId).querySelector('.name').textContent + let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`; + const notification = this._notify(title, 'Click to show'); + } + } + + _download(notification) { + $('download-btn').click(); + notification.close(); + } + + _copyText(message, notification) { + if (navigator.clipboard.writeText(message)) { + notification.close(); + this._notify('Copied text to clipboard'); + } else { + this._notify('Writing to clipboard failed. Copy manually!'); + + } + } + + _bind(notification, handler) { + if (notification.then) { + notification.then(_ => serviceWorker.getNotifications().then(_ => { + serviceWorker.addEventListener('notificationclick', handler); + })); + } else { + notification.onclick = handler; + } + } +} + +class NetworkStatusUI { + + constructor() { + Events.on('offline', _ => this._showOfflineMessage()); + Events.on('online', _ => this._showOnlineMessage()); + if (!navigator.onLine) this._showOfflineMessage(); + } + + _showOfflineMessage() { + Events.fire('notify-user', 'You are offline'); + } + + _showOnlineMessage() { + Events.fire('notify-user', 'You are back online'); + } +} + +class WebShareTargetUI { + constructor() { + const urlParams = new URL(window.location).searchParams; + const share_target_type = urlParams.get("share-target") + if (share_target_type) { + if (share_target_type === "text") { + const title = urlParams.get('title') || ''; + const text = urlParams.get('text') || ''; + const url = urlParams.get('url') || ''; + let shareTargetText; + + if (url) { + shareTargetText = url; // we share only the link - no text. + } else if (title && text) { + shareTargetText = title + '\r\n' + text; + } else { + shareTargetText = title + text; + } + + Events.fire('activate-paste-mode', {files: [], text: shareTargetText}) + } else if (share_target_type === "files") { + let openRequest = window.indexedDB.open('moreload_store') + openRequest.onsuccess = e => { + const db = e.target.result; + const tx = db.transaction('share_target_files', 'readwrite'); + const store = tx.objectStore('share_target_files'); + const request = store.getAll(); + request.onsuccess = _ => { + const fileObjects = request.result; + let filesReceived = []; + for (let i=0; i db.close(); + + Events.fire('activate-paste-mode', {files: filesReceived, text: ""}) + } + } + } + window.history.replaceState({}, "Rewrite URL", '/'); + } + } +} + +class WebFileHandlersUI { + constructor() { + const urlParams = new URL(window.location).searchParams; + if (urlParams.has("file_handler") && "launchQueue" in window) { + launchQueue.setConsumer(async launchParams => { + console.log("Launched with: ", launchParams); + if (!launchParams.files.length) + return; + let files = []; + + for (let i=0; i NoSleepUI.disable(), 10000); + } + } + + static disable() { + if ($$('x-peer[status]') === null) { + clearInterval(NoSleepUI._interval); + NoSleepUI._nosleep.disable(); + } + } +} + +class PersistentStorage { + constructor() { + if (!('indexedDB' in window)) { + PersistentStorage.logBrowserNotCapable(); + return; + } + const DBOpenRequest = window.indexedDB.open('moreload_store', 4); + DBOpenRequest.onerror = (e) => { + PersistentStorage.logBrowserNotCapable(); + console.log('Error initializing database: '); + console.log(e) + }; + DBOpenRequest.onsuccess = () => { + console.log('Database initialised.'); + }; + DBOpenRequest.onupgradeneeded = (e) => { + const db = e.target.result; + const txn = e.target.transaction; + + db.onerror = e => console.log('Error loading database: ' + e); + + console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`); + + if (e.oldVersion === 0) { + // initiate v1 + db.createObjectStore('keyval'); + let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true}); + roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true }); + } + if (e.oldVersion <= 1) { + // migrate to v2 + db.createObjectStore('share_target_files'); + } + if (e.oldVersion <= 2) { + // migrate to v3 + db.deleteObjectStore('share_target_files'); + db.createObjectStore('share_target_files', {autoIncrement: true}); + } + if (e.oldVersion <= 3) { + // migrate to v4 + let roomSecretsObjectStore4 = txn.objectStore('room_secrets'); + roomSecretsObjectStore4.createIndex('display_name', 'display_name'); + roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept'); + } + } + } + + static logBrowserNotCapable() { + console.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed."); + } + + static set(key, value) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('keyval', 'readwrite'); + const objectStore = transaction.objectStore('keyval'); + const objectStoreRequest = objectStore.put(value, key); + objectStoreRequest.onsuccess = _ => { + console.log(`Request successful. Added key-pair: ${key} - ${value}`); + resolve(value); + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static get(key) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('keyval', 'readonly'); + const objectStore = transaction.objectStore('keyval'); + const objectStoreRequest = objectStore.get(key); + objectStoreRequest.onsuccess = _ => { + console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`); + resolve(objectStoreRequest.result); + } + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }); + } + + static delete(key) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('keyval', 'readwrite'); + const objectStore = transaction.objectStore('keyval'); + const objectStoreRequest = objectStore.delete(key); + objectStoreRequest.onsuccess = _ => { + console.log(`Request successful. Deleted key: ${key}`); + resolve(); + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static addRoomSecret(roomSecret, displayName, deviceName) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequest = objectStore.add({ + 'secret': roomSecret, + 'display_name': displayName, + 'device_name': deviceName, + 'auto_accept': false + }); + objectStoreRequest.onsuccess = e => { + console.log(`Request successful. RoomSecret added: ${e.target.result}`); + resolve(); + } + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static async getAllRoomSecrets() { + try { + const roomSecrets = await this.getAllRoomSecretEntries(); + let secrets = []; + for (let i = 0; i < roomSecrets.length; i++) { + secrets.push(roomSecrets[i].secret); + } + console.log(`Request successful. Retrieved ${secrets.length} room_secrets`); + return(secrets); + } catch (e) { + this.logBrowserNotCapable(); + return false; + } + } + + static getAllRoomSecretEntries() { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readonly'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequest = objectStore.getAll(); + objectStoreRequest.onsuccess = e => { + resolve(e.target.result); + } + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }); + } + + static getRoomSecretEntry(roomSecret) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readonly'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret); + objectStoreRequestKey.onsuccess = e => { + const key = e.target.result; + if (!key) { + console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`); + resolve(); + return; + } + const objectStoreRequestRetrieval = objectStore.get(key); + objectStoreRequestRetrieval.onsuccess = e => { + console.log(`Request successful. Retrieved entry for room_secret: ${key}`); + resolve({ + "entry": e.target.result, + "key": key + }); + } + objectStoreRequestRetrieval.onerror = (e) => { + reject(e); + } + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }); + } + + static deleteRoomSecret(roomSecret) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret); + objectStoreRequestKey.onsuccess = e => { + if (!e.target.result) { + console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`); + resolve(); + return; + } + const key = e.target.result; + const objectStoreRequestDeletion = objectStore.delete(key); + objectStoreRequestDeletion.onsuccess = _ => { + console.log(`Request successful. Deleted room_secret: ${key}`); + resolve(roomSecret); + } + objectStoreRequestDeletion.onerror = (e) => { + reject(e); + } + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static clearRoomSecrets() { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + const objectStoreRequest = objectStore.clear(); + objectStoreRequest.onsuccess = _ => { + console.log('Request successful. All room_secrets cleared'); + resolve(); + }; + } + DBOpenRequest.onerror = (e) => { + reject(e); + } + }) + } + + static updateRoomSecretNames(roomSecret, displayName, deviceName) { + return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName); + } + + static updateRoomSecretAutoAccept(roomSecret, autoAccept) { + return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept); + } + + static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) { + return new Promise((resolve, reject) => { + const DBOpenRequest = window.indexedDB.open('moreload_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + this.getRoomSecretEntry(roomSecret) + .then(roomSecretEntry => { + if (!roomSecretEntry) { + resolve(false); + return; + } + const transaction = db.transaction('room_secrets', 'readwrite'); + const objectStore = transaction.objectStore('room_secrets'); + // Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers + const updatedRoomSecretEntry = { + 'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret, + 'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name, + 'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name, + 'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept + }; + + const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key); + + objectStoreRequestUpdate.onsuccess = e => { + console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`); + resolve({ + "entry": updatedRoomSecretEntry, + "key": roomSecretEntry.key + }); + } + + objectStoreRequestUpdate.onerror = (e) => { + reject(e); + } + }) + .catch(e => reject(e)); + }; + + DBOpenRequest.onerror = e => reject(e); + }) + } +} + +class BrowserTabsConnector { + constructor() { + this.bc = new BroadcastChannel('moreload'); + this.bc.addEventListener('message', e => this._onMessage(e)); + Events.on('broadcast-send', e => this._broadcastSend(e.detail)); + } + + _broadcastSend(message) { + this.bc.postMessage(message); + } + + _onMessage(e) { + console.log('Broadcast:', e.data) + switch (e.data.type) { + case 'self-display-name-changed': + Events.fire('self-display-name-changed', e.data.detail); + break; + } + } + + static peerIsSameBrowser(peerId) { + let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser")); + return peerIdsBrowser + ? peerIdsBrowser.indexOf(peerId) !== -1 + : false; + } + + static async addPeerIdToLocalStorage() { + const peerId = sessionStorage.getItem("peerId"); + if (!peerId) return false; + + let peerIdsBrowser = []; + let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser")); + + if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld); + peerIdsBrowser.push(peerId); + peerIdsBrowser = peerIdsBrowser.filter(onlyUnique); + localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser)); + + return peerIdsBrowser; + } + + static async removePeerIdFromLocalStorage(peerId) { + let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser")); + const index = peerIdsBrowser.indexOf(peerId); + peerIdsBrowser.splice(index, 1); + localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser)); + return peerId; + } + + + static async removeOtherPeerIdsFromLocalStorage() { + const peerId = sessionStorage.getItem("peerId"); + if (!peerId) return false; + + let peerIdsBrowser = [peerId]; + localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser)); + return peerIdsBrowser; + } +} + +class MoreLoad { + constructor() { + Events.on('load', _ => { + const server = new ServerConnection(); + const peers = new PeersManager(server); + const peersUI = new PeersUI(); + const receiveFileDialog = new ReceiveFileDialog(); + const receiveRequestDialog = new ReceiveRequestDialog(); + const sendTextDialog = new SendTextDialog(); + const receiveTextDialog = new ReceiveTextDialog(); + const pairDeviceDialog = new PairDeviceDialog(); + const clearDevicesDialog = new EditPairedDevicesDialog(); + const base64ZipDialog = new Base64ZipDialog(); + const toast = new Toast(); + const notifications = new Notifications(); + const networkStatusUI = new NetworkStatusUI(); + const webShareTargetUI = new WebShareTargetUI(); + const webFileHandlersUI = new WebFileHandlersUI(); + const noSleepUI = new NoSleepUI(); + const broadCast = new BrowserTabsConnector(); + }); + } +} + +const persistentStorage = new PersistentStorage(); +const moreload = new MoreLoad(); + + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/service-worker.js') + .then(serviceWorker => { + console.log('Service Worker registered'); + window.serviceWorker = serviceWorker + }); +} + +window.addEventListener('beforeinstallprompt', e => { + if (!window.matchMedia('(display-mode: minimal-ui)').matches) { + // only display install btn when installed + const btn = document.querySelector('#install') + btn.hidden = false; + btn.onclick = _ => e.prompt(); + } + return e.preventDefault(); +}); + +// Background Circles +Events.on('load', () => { + let c = $$('canvas'); + let cCtx = c.getContext('2d'); + let x0, y0, w, h, dw, offset; + + function init() { + let oldW = w; + let oldH = h; + let oldOffset = offset + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; + offset = $$('footer').offsetHeight - 33; + + if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed + + c.width = w; + c.height = h; + x0 = w / 2; + y0 = h - offset; + dw = Math.round(Math.max(w, h, 1000) / 13); + + drawCircles(cCtx, dw); + } + + Events.on('bg-resize', _ => init()); + window.onresize = _ => Events.fire('bg-resize'); + + function drawCircle(ctx, radius) { + ctx.beginPath(); + ctx.lineWidth = 2; + let opacity = 0.3 * (1 - 1.2 * radius / Math.max(w, h)); + ctx.strokeStyle = `rgba(128, 128, 128, ${opacity})`; + ctx.arc(x0, y0, radius, 0, 2 * Math.PI); + ctx.stroke(); + } + + function drawCircles(ctx, frame) { + ctx.clearRect(0, 0, w, h); + for (let i = 0; i < 13; i++) { + drawCircle(ctx, dw * i + frame + 33); + } + } + + init(); +}); + +document.changeFavicon = function (src) { + document.querySelector('[rel="icon"]').href = src; + document.querySelector('[rel="shortcut icon"]').href = src; +} + +// close About MoreLoad page on Escape +window.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + window.location.hash = '#'; + } +}); + +Notifications.PERMISSION_ERROR = ` +Notifications permission has been blocked +as the user has dismissed the permission prompt several times. +This can be reset in Page Info +which can be accessed by clicking the lock icon next to the URL.`; diff --git a/public_included_ws_fallback/scripts/util.js b/public_included_ws_fallback/scripts/util.js new file mode 100644 index 0000000..e0cbf24 --- /dev/null +++ b/public_included_ws_fallback/scripts/util.js @@ -0,0 +1,424 @@ +// Polyfill for Navigator.clipboard.writeText +if (!navigator.clipboard) { + navigator.clipboard = { + writeText: text => { + + // A contains the text to copy + const span = document.createElement('span'); + span.textContent = text; + span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines + + // Paint the span outside the viewport + span.style.position = 'absolute'; + span.style.left = '-9999px'; + span.style.top = '-9999px'; + + const win = window; + const selection = win.getSelection(); + win.document.body.appendChild(span); + + const range = win.document.createRange(); + selection.removeAllRanges(); + range.selectNode(span); + selection.addRange(range); + + let success = false; + try { + success = win.document.execCommand('copy'); + } catch (err) { + return Promise.error(); + } + + selection.removeAllRanges(); + span.remove(); + + return Promise.resolve(); + } + } +} + +const zipper = (() => { + + let zipWriter; + return { + createNewZipWriter() { + zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, level: 0 }); + }, + addFile(file, options) { + return zipWriter.add(file.name, new zip.BlobReader(file), options); + }, + async getBlobURL() { + if (zipWriter) { + const blobURL = URL.createObjectURL(await zipWriter.close()); + zipWriter = null; + return blobURL; + } else { + throw new Error("Zip file closed"); + } + }, + async getZipFile(filename = "archive.zip") { + if (zipWriter) { + const file = new File([await zipWriter.close()], filename, {type: "application/zip"}); + zipWriter = null; + return file; + } else { + throw new Error("Zip file closed"); + } + }, + async getEntries(file, options) { + return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options); + }, + async getData(entry, options) { + return await entry.getData(new zip.BlobWriter(), options); + }, + }; + +})(); + +const mime = (() => { + + return { + getMimeByFilename(filename) { + try { + const arr = filename.split('.'); + const suffix = arr[arr.length - 1].toLowerCase(); + return { + "cpl": "application/cpl+xml", + "gpx": "application/gpx+xml", + "gz": "application/gzip", + "jar": "application/java-archive", + "war": "application/java-archive", + "ear": "application/java-archive", + "class": "application/java-vm", + "js": "application/javascript", + "mjs": "application/javascript", + "json": "application/json", + "map": "application/json", + "webmanifest": "application/manifest+json", + "doc": "application/msword", + "dot": "application/msword", + "wiz": "application/msword", + "bin": "application/octet-stream", + "dms": "application/octet-stream", + "lrf": "application/octet-stream", + "mar": "application/octet-stream", + "so": "application/octet-stream", + "dist": "application/octet-stream", + "distz": "application/octet-stream", + "pkg": "application/octet-stream", + "bpk": "application/octet-stream", + "dump": "application/octet-stream", + "elc": "application/octet-stream", + "deploy": "application/octet-stream", + "img": "application/octet-stream", + "msp": "application/octet-stream", + "msm": "application/octet-stream", + "buffer": "application/octet-stream", + "oda": "application/oda", + "oxps": "application/oxps", + "pdf": "application/pdf", + "asc": "application/pgp-signature", + "sig": "application/pgp-signature", + "prf": "application/pics-rules", + "p7c": "application/pkcs7-mime", + "cer": "application/pkix-cert", + "ai": "application/postscript", + "eps": "application/postscript", + "ps": "application/postscript", + "apk": "application/vnd.android.package-archive", + "m3u8": "application/vnd.apple.mpegurl", + "pkpass": "application/vnd.apple.pkpass", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "cab": "application/vnd.ms-cab-compressed", + "xls": "application/vnd.ms-excel", + "xlm": "application/vnd.ms-excel", + "xla": "application/vnd.ms-excel", + "xlc": "application/vnd.ms-excel", + "xlt": "application/vnd.ms-excel", + "xlw": "application/vnd.ms-excel", + "msg": "application/vnd.ms-outlook", + "ppt": "application/vnd.ms-powerpoint", + "pot": "application/vnd.ms-powerpoint", + "ppa": "application/vnd.ms-powerpoint", + "pps": "application/vnd.ms-powerpoint", + "pwz": "application/vnd.ms-powerpoint", + "mpp": "application/vnd.ms-project", + "mpt": "application/vnd.ms-project", + "xps": "application/vnd.ms-xpsdocument", + "odb": "application/vnd.oasis.opendocument.database", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "odt": "application/vnd.oasis.opendocument.text", + "osm": "application/vnd.openstreetmap.data+xml", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "pcap": "application/vnd.tcpdump.pcap", + "cap": "application/vnd.tcpdump.pcap", + "dmp": "application/vnd.tcpdump.pcap", + "wpd": "application/vnd.wordperfect", + "wasm": "application/wasm", + "7z": "application/x-7z-compressed", + "dmg": "application/x-apple-diskimage", + "bcpio": "application/x-bcpio", + "torrent": "application/x-bittorrent", + "cbr": "application/x-cbr", + "cba": "application/x-cbr", + "cbt": "application/x-cbr", + "cbz": "application/x-cbr", + "cb7": "application/x-cbr", + "vcd": "application/x-cdlink", + "crx": "application/x-chrome-extension", + "cpio": "application/x-cpio", + "csh": "application/x-csh", + "deb": "application/x-debian-package", + "udeb": "application/x-debian-package", + "dvi": "application/x-dvi", + "arc": "application/x-freearc", + "gtar": "application/x-gtar", + "hdf": "application/x-hdf", + "h5": "application/x-hdf5", + "php": "application/x-httpd-php", + "iso": "application/x-iso9660-image", + "key": "application/x-iwork-keynote-sffkey", + "numbers": "application/x-iwork-numbers-sffnumbers", + "pages": "application/x-iwork-pages-sffpages", + "latex": "application/x-latex", + "run": "application/x-makeself", + "mif": "application/x-mif", + "lnk": "application/x-ms-shortcut", + "mdb": "application/x-msaccess", + "exe": "application/x-msdownload", + "dll": "application/x-msdownload", + "com": "application/x-msdownload", + "bat": "application/x-msdownload", + "msi": "application/x-msdownload", + "pub": "application/x-mspublisher", + "cdf": "application/x-netcdf", + "nc": "application/x-netcdf", + "pl": "application/x-perl", + "pm": "application/x-perl", + "prc": "application/x-pilot", + "pdb": "application/x-pilot", + "p12": "application/x-pkcs12", + "pfx": "application/x-pkcs12", + "ram": "application/x-pn-realaudio", + "pyc": "application/x-python-code", + "pyo": "application/x-python-code", + "rar": "application/x-rar-compressed", + "rpm": "application/x-redhat-package-manager", + "sh": "application/x-sh", + "shar": "application/x-shar", + "swf": "application/x-shockwave-flash", + "sql": "application/x-sql", + "srt": "application/x-subrip", + "sv4cpio": "application/x-sv4cpio", + "sv4crc": "application/x-sv4crc", + "gam": "application/x-tads", + "tar": "application/x-tar", + "tcl": "application/x-tcl", + "tex": "application/x-tex", + "roff": "application/x-troff", + "t": "application/x-troff", + "tr": "application/x-troff", + "man": "application/x-troff-man", + "me": "application/x-troff-me", + "ms": "application/x-troff-ms", + "ustar": "application/x-ustar", + "src": "application/x-wais-source", + "xpi": "application/x-xpinstall", + "xhtml": "application/xhtml+xml", + "xht": "application/xhtml+xml", + "xsl": "application/xml", + "rdf": "application/xml", + "wsdl": "application/xml", + "xpdl": "application/xml", + "zip": "application/zip", + "3gp": "audio/3gp", + "3gpp": "audio/3gpp", + "3g2": "audio/3gpp2", + "3gpp2": "audio/3gpp2", + "aac": "audio/aac", + "adts": "audio/aac", + "loas": "audio/aac", + "ass": "audio/aac", + "au": "audio/basic", + "snd": "audio/basic", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "rmi": "audio/midi", + "mpga": "audio/mpeg", + "mp2": "audio/mpeg", + "mp2a": "audio/mpeg", + "mp3": "audio/mpeg", + "m2a": "audio/mpeg", + "m3a": "audio/mpeg", + "oga": "audio/ogg", + "ogg": "audio/ogg", + "spx": "audio/ogg", + "opus": "audio/opus", + "aif": "audio/x-aiff", + "aifc": "audio/x-aiff", + "aiff": "audio/x-aiff", + "flac": "audio/x-flac", + "m4a": "audio/x-m4a", + "m3u": "audio/x-mpegurl", + "wma": "audio/x-ms-wma", + "ra": "audio/x-pn-realaudio", + "wav": "audio/x-wav", + "otf": "font/otf", + "ttf": "font/ttf", + "woff": "font/woff", + "woff2": "font/woff2", + "emf": "image/emf", + "gif": "image/gif", + "heic": "image/heic", + "heif": "image/heif", + "ief": "image/ief", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "pict": "image/pict", + "pct": "image/pict", + "pic": "image/pict", + "png": "image/png", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "psd": "image/vnd.adobe.photoshop", + "djvu": "image/vnd.djvu", + "djv": "image/vnd.djvu", + "dwg": "image/vnd.dwg", + "dxf": "image/vnd.dxf", + "dds": "image/vnd.ms-dds", + "webp": "image/webp", + "3ds": "image/x-3ds", + "ras": "image/x-cmu-raster", + "ico": "image/x-icon", + "bmp": "image/x-ms-bmp", + "pnm": "image/x-portable-anymap", + "pbm": "image/x-portable-bitmap", + "pgm": "image/x-portable-graymap", + "ppm": "image/x-portable-pixmap", + "rgb": "image/x-rgb", + "tga": "image/x-tga", + "xbm": "image/x-xbitmap", + "xpm": "image/x-xpixmap", + "xwd": "image/x-xwindowdump", + "eml": "message/rfc822", + "mht": "message/rfc822", + "mhtml": "message/rfc822", + "nws": "message/rfc822", + "obj": "model/obj", + "stl": "model/stl", + "dae": "model/vnd.collada+xml", + "ics": "text/calendar", + "ifb": "text/calendar", + "css": "text/css", + "csv": "text/csv", + "html": "text/html", + "htm": "text/html", + "shtml": "text/html", + "markdown": "text/markdown", + "md": "text/markdown", + "txt": "text/plain", + "text": "text/plain", + "conf": "text/plain", + "def": "text/plain", + "list": "text/plain", + "log": "text/plain", + "in": "text/plain", + "ini": "text/plain", + "rtx": "text/richtext", + "rtf": "text/rtf", + "tsv": "text/tab-separated-values", + "c": "text/x-c", + "cc": "text/x-c", + "cxx": "text/x-c", + "cpp": "text/x-c", + "h": "text/x-c", + "hh": "text/x-c", + "dic": "text/x-c", + "java": "text/x-java-source", + "lua": "text/x-lua", + "py": "text/x-python", + "etx": "text/x-setext", + "sgm": "text/x-sgml", + "sgml": "text/x-sgml", + "vcf": "text/x-vcard", + "xml": "text/xml", + "xul": "text/xul", + "yaml": "text/yaml", + "yml": "text/yaml", + "ts": "video/mp2t", + "mp4": "video/mp4", + "mp4v": "video/mp4", + "mpg4": "video/mp4", + "mpeg": "video/mpeg", + "m1v": "video/mpeg", + "mpa": "video/mpeg", + "mpe": "video/mpeg", + "mpg": "video/mpeg", + "mov": "video/quicktime", + "qt": "video/quicktime", + "webm": "video/webm", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "asf": "video/x-ms-asf", + "asx": "video/x-ms-asf", + "vob": "video/x-ms-vob", + "wmv": "video/x-ms-wmv", + "avi": "video/x-msvideo", + "*": "video/x-sgi-movie", + }[suffix] || ''; + } catch (e) { + console.error(e); + return ''; + } + } + }; + +})(); + +/* + cyrb53 (c) 2018 bryc (github.com/bryc) + A fast and simple hash function with decent collision resistance. + Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. + Public domain. Attribution appreciated. +*/ +const cyrb53 = function(str, seed = 0) { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909); + h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1>>>0); +}; + +function onlyUnique (value, index, array) { + return array.indexOf(value) === index; +} + +function arrayBufferToBase64(buffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa( binary ); +} + +function base64ToArrayBuffer(base64) { + var binary_string = window.atob(base64); + var len = binary_string.length; + var bytes = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; +} diff --git a/public_included_ws_fallback/scripts/zip.min.js b/public_included_ws_fallback/scripts/zip.min.js new file mode 100644 index 0000000..b9db6df --- /dev/null +++ b/public_included_ws_fallback/scripts/zip.min.js @@ -0,0 +1 @@ +((e,t)=>{"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).zip={})})(this,(function(e){"use strict";const{Array:t,Object:n,String:r,Number:s,BigInt:i,Math:a,Date:o,Map:c,Set:l,Response:u,URL:f,Error:d,Uint8Array:w,Uint16Array:h,Uint32Array:p,DataView:g,Blob:m,Promise:y,TextEncoder:b,TextDecoder:S,document:k,crypto:z,btoa:x,TransformStream:v,ReadableStream:C,WritableStream:_,CompressionStream:D,DecompressionStream:R,navigator:F,Worker:E}="undefined"!=typeof globalThis?globalThis:this||self,I=4294967295,A=65535,T=67324752,H=134695760,O=33639248,U=101075792,N=117853008,W=21589,q=2048,L="/",M=new o(2107,11,31),P=new o(1980,0,1),V=void 0,B="undefined",K="function";class Y{constructor(e){return class extends v{constructor(t,n){const r=new e(n);super({transform(e,t){t.enqueue(r.append(e))},flush(e){const t=r.flush();t&&e.enqueue(t)}})}}}}let Z=2;try{typeof F!=B&&F.hardwareConcurrency&&(Z=F.hardwareConcurrency)}catch(e){}const X={chunkSize:524288,maxWorkers:Z,terminateWorkerTimeout:5e3,useWebWorkers:!0,useCompressionStream:!0,workerScripts:V,CompressionStreamNative:typeof D!=B&&D,DecompressionStreamNative:typeof R!=B&&R},G=n.assign({},X);function j(){return G}function J(e){return a.max(e.chunkSize,64)}function Q(e){const{baseURL:n,chunkSize:r,maxWorkers:s,terminateWorkerTimeout:i,useCompressionStream:a,useWebWorkers:o,Deflate:c,Inflate:l,CompressionStream:u,DecompressionStream:f,workerScripts:w}=e;if($("baseURL",n),$("chunkSize",r),$("maxWorkers",s),$("terminateWorkerTimeout",i),$("useCompressionStream",a),$("useWebWorkers",o),c&&(G.CompressionStream=new Y(c)),l&&(G.DecompressionStream=new Y(l)),$("CompressionStream",u),$("DecompressionStream",f),w!==V){const{deflate:e,inflate:n}=w;if((e||n)&&(G.workerScripts||(G.workerScripts={})),e){if(!t.isArray(e))throw new d("workerScripts.deflate must be an array");G.workerScripts.deflate=e}if(n){if(!t.isArray(n))throw new d("workerScripts.inflate must be an array");G.workerScripts.inflate=n}}}function $(e,t){t!==V&&(G[e]=t)}function ee(e,t,r){return class{constructor(s){const i=this;n.hasOwn(s,"level")&&void 0===s.level&&delete s.level,i.codec=new e(n.assign({},t,s)),r(i.codec,(e=>{if(i.pendingData){const t=i.pendingData;i.pendingData=new w(t.length+e.length);const{pendingData:n}=i;n.set(t,0),n.set(e,t.length)}else i.pendingData=new w(e)}))}append(e){return this.codec.push(e),s(this)}flush(){return this.codec.push(new w,!0),s(this)}};function s(e){if(e.pendingData){const t=e.pendingData;return e.pendingData=null,t}return new w}}const te=[];for(let e=0;256>e;e++){let t=e;for(let e=0;8>e;e++)1&t?t=t>>>1^3988292384:t>>>=1;te[e]=t}class ne{constructor(e){this.crc=e||-1}append(e){let t=0|this.crc;for(let n=0,r=0|e.length;r>n;n++)t=t>>>8^te[255&(t^e[n])];this.crc=t}get(){return~this.crc}}class re extends v{constructor(){const e=new ne;super({transform(t){e.append(t)},flush(t){const n=new w(4);new g(n.buffer).setUint32(0,e.get()),t.enqueue(n)}})}}function se(e){if(void 0===b){const t=new w((e=unescape(encodeURIComponent(e))).length);for(let n=0;n0&&t&&(e[n-1]=ie.partial(t,e[n-1]&2147483648>>t-1,1)),e},partial:(e,t,n)=>32===e?t:(n?0|t:t<<32-e)+1099511627776*e,getPartial:e=>a.round(e/1099511627776)||32,_shiftRight(e,t,n,r){for(void 0===r&&(r=[]);t>=32;t-=32)r.push(n),n=0;if(0===t)return r.concat(e);for(let s=0;s>>t),n=e[s]<<32-t;const s=e.length?e[e.length-1]:0,i=ie.getPartial(s);return r.push(ie.partial(t+i&31,t+i>32?n:r.pop(),1)),r}},ae={bytes:{fromBits(e){const t=ie.bitLength(e)/8,n=new w(t);let r;for(let s=0;t>s;s++)0==(3&s)&&(r=e[s/4]),n[s]=r>>>24,r<<=8;return n},toBits(e){const t=[];let n,r=0;for(n=0;n{let t=987654321;const n=4294967295;return()=>(t=36969*(65535&t)+(t>>16)&n,(((t<<16)+(e=18e3*(65535&e)+(e>>16)&n)&n)/4294967296+.5)*(a.random()>.5?1:-1))};for(let r,s=0;snew ce.hmacSha1(ae.bytes.toBits(e)),pbkdf2(e,t,n,r){if(n=n||1e4,0>r||0>n)throw new d("invalid params to pbkdf2");const s=1+(r>>5)<<2;let i,a,o,c,l;const u=new ArrayBuffer(s),f=new g(u);let w=0;const h=ie;for(t=ae.bytes.toBits(t),l=1;(s||1)>w;l++){for(i=a=e.encrypt(h.concat(t,[l])),o=1;n>o;o++)for(a=e.encrypt(a),c=0;cw&&o9007199254740991)throw new d("Cannot hash more than 2^53 - 1 bits");const i=new p(n);let a=0;for(let e=t.blockSize+r-(t.blockSize+r&t.blockSize-1);s>=e;e+=t.blockSize)t._block(i.subarray(16*a,16*(a+1))),a+=1;return n.splice(0,16*a),t}finalize(){const e=this;let t=e._buffer;const n=e._h;t=ie.concat(t,[ie.partial(1,1)]);for(let e=t.length+2;15&e;e++)t.push(0);for(t.push(a.floor(e._length/4294967296)),t.push(0|e._length);t.length;)e._block(t.splice(0,16));return e.reset(),n}_f(e,t,n,r){return e>19?e>39?e>59?e>79?void 0:t^n^r:t&n|t&r|n&r:t^n^r:t&n|~t&r}_S(e,t){return t<>>32-e}_block(e){const n=this,r=n._h,s=t(80);for(let t=0;16>t;t++)s[t]=e[t];let i=r[0],o=r[1],c=r[2],l=r[3],u=r[4];for(let e=0;79>=e;e++){16>e||(s[e]=n._S(1,s[e-3]^s[e-8]^s[e-14]^s[e-16]));const t=n._S(5,i)+n._f(e,o,c,l)+u+s[e]+n._key[a.floor(e/20)]|0;u=l,l=c,c=n._S(30,o),o=i,i=t}r[0]=r[0]+i|0,r[1]=r[1]+o|0,r[2]=r[2]+c|0,r[3]=r[3]+l|0,r[4]=r[4]+u|0}},s=[[],[]];n._baseHash=[new r,new r];const i=n._baseHash[0].blockSize/32;e.length>i&&(e=r.hash(e));for(let t=0;i>t;t++)s[0][t]=909522486^e[t],s[1][t]=1549556828^e[t];n._baseHash[0].update(s[0]),n._baseHash[1].update(s[1]),n._resultHash=new r(n._baseHash[0])}reset(){const e=this;e._resultHash=new e._hash(e._baseHash[0]),e._updated=!1}update(e){this._updated=!0,this._resultHash.update(e)}digest(){const e=this,t=e._resultHash.finalize(),n=new e._hash(e._baseHash[1]).update(t).finalize();return e.reset(),n}encrypt(e){if(this._updated)throw new d("encrypt on already updated hmac called!");return this.update(e),this.digest(e)}}},le=void 0!==z&&"function"==typeof z.getRandomValues,ue="Invalid password",fe="Invalid signature";function de(e){return le?z.getRandomValues(e):oe.getRandomValues(e)}const we=16,he={name:"PBKDF2"},pe=n.assign({hash:{name:"HMAC"}},he),ge=n.assign({iterations:1e3,hash:{name:"SHA-1"}},he),me=["deriveBits"],ye=[8,12,16],be=[16,24,32],Se=10,ke=[0,0,0,0],ze="undefined",xe="function",ve=typeof z!=ze,Ce=ve&&z.subtle,_e=ve&&typeof Ce!=ze,De=ae.bytes,Re=class{constructor(e){const t=this;t._tables=[[[],[],[],[],[]],[[],[],[],[],[]]],t._tables[0][0][0]||t._precompute();const n=t._tables[0][4],r=t._tables[1],s=e.length;let i,a,o,c=1;if(4!==s&&6!==s&&8!==s)throw new d("invalid aes key size");for(t._key=[a=e.slice(0),o=[]],i=s;4*s+28>i;i++){let e=a[i-1];(i%s==0||8===s&&i%s==4)&&(e=n[e>>>24]<<24^n[e>>16&255]<<16^n[e>>8&255]<<8^n[255&e],i%s==0&&(e=e<<8^e>>>24^c<<24,c=c<<1^283*(c>>7))),a[i]=a[i-s]^e}for(let e=0;i;e++,i--){const t=a[3&e?i:i-4];o[e]=4>=i||4>e?t:r[0][n[t>>>24]]^r[1][n[t>>16&255]]^r[2][n[t>>8&255]]^r[3][n[255&t]]}}encrypt(e){return this._crypt(e,0)}decrypt(e){return this._crypt(e,1)}_precompute(){const e=this._tables[0],t=this._tables[1],n=e[4],r=t[4],s=[],i=[];let a,o,c,l;for(let e=0;256>e;e++)i[(s[e]=e<<1^283*(e>>7))^e]=e;for(let u=a=0;!n[u];u^=o||1,a=i[a]||1){let i=a^a<<1^a<<2^a<<3^a<<4;i=i>>8^255&i^99,n[u]=i,r[i]=u,l=s[c=s[o=s[u]]];let f=16843009*l^65537*c^257*o^16843008*u,d=257*s[i]^16843008*i;for(let n=0;4>n;n++)e[n][u]=d=d<<24^d>>>8,t[n][i]=f=f<<24^f>>>8}for(let n=0;5>n;n++)e[n]=e[n].slice(0),t[n]=t[n].slice(0)}_crypt(e,t){if(4!==e.length)throw new d("invalid aes block size");const n=this._key[t],r=n.length/4-2,s=[0,0,0,0],i=this._tables[t],a=i[0],o=i[1],c=i[2],l=i[3],u=i[4];let f,w,h,p=e[0]^n[0],g=e[t?3:1]^n[1],m=e[2]^n[2],y=e[t?1:3]^n[3],b=4;for(let e=0;r>e;e++)f=a[p>>>24]^o[g>>16&255]^c[m>>8&255]^l[255&y]^n[b],w=a[g>>>24]^o[m>>16&255]^c[y>>8&255]^l[255&p]^n[b+1],h=a[m>>>24]^o[y>>16&255]^c[p>>8&255]^l[255&g]^n[b+2],y=a[y>>>24]^o[p>>16&255]^c[g>>8&255]^l[255&m]^n[b+3],b+=4,p=f,g=w,m=h;for(let e=0;4>e;e++)s[t?3&-e:e]=u[p>>>24]<<24^u[g>>16&255]<<16^u[m>>8&255]<<8^u[255&y]^n[b++],f=p,p=g,g=m,m=y,y=f;return s}},Fe=class{constructor(e,t){this._prf=e,this._initIv=t,this._iv=t}reset(){this._iv=this._initIv}update(e){return this.calculate(this._prf,e,this._iv)}incWord(e){if(255==(e>>24&255)){let t=e>>16&255,n=e>>8&255,r=255&e;255===t?(t=0,255===n?(n=0,255===r?r=0:++r):++n):++t,e=0,e+=t<<16,e+=n<<8,e+=r}else e+=1<<24;return e}incCounter(e){0===(e[0]=this.incWord(e[0]))&&(e[1]=this.incWord(e[1]))}calculate(e,t,n){let r;if(!(r=t.length))return[];const s=ie.bitLength(t);for(let s=0;r>s;s+=4){this.incCounter(n);const r=e.encrypt(n);t[s]^=r[0],t[s+1]^=r[1],t[s+2]^=r[2],t[s+3]^=r[3]}return ie.clamp(t,s)}},Ee=ce.hmacSha1;let Ie=ve&&_e&&typeof Ce.importKey==xe,Ae=ve&&_e&&typeof Ce.deriveBits==xe;class Te extends v{constructor({password:e,signed:t,encryptionStrength:r}){super({start(){n.assign(this,{ready:new y((e=>this.resolveReady=e)),password:e,signed:t,strength:r-1,pending:new w})},async transform(e,t){const n=this,{password:r,strength:s,resolveReady:i,ready:a}=n;r?(await(async(e,t,n,r)=>{const s=await Ue(e,t,n,We(r,0,ye[t])),i=We(r,ye[t]);if(s[0]!=i[0]||s[1]!=i[1])throw new d(ue)})(n,s,r,We(e,0,ye[s]+2)),e=We(e,ye[s]+2),i()):await a;const o=new w(e.length-Se-(e.length-Se)%we);t.enqueue(Oe(n,e,o,0,Se,!0))},async flush(e){const{signed:t,ctr:n,hmac:r,pending:s,ready:i}=this;await i;const a=We(s,0,s.length-Se),o=We(s,s.length-Se);let c=new w;if(a.length){const e=Le(De,a);r.update(e);const t=n.update(e);c=qe(De,t)}if(t){const e=We(qe(De,r.digest()),0,Se);for(let t=0;Se>t;t++)if(e[t]!=o[t])throw new d(fe)}e.enqueue(c)}})}}class He extends v{constructor({password:e,encryptionStrength:t}){let r;super({start(){n.assign(this,{ready:new y((e=>this.resolveReady=e)),password:e,strength:t-1,pending:new w})},async transform(e,t){const n=this,{password:r,strength:s,resolveReady:i,ready:a}=n;let o=new w;r?(o=await(async(e,t,n)=>{const r=de(new w(ye[t]));return Ne(r,await Ue(e,t,n,r))})(n,s,r),i()):await a;const c=new w(o.length+e.length-e.length%we);c.set(o,0),t.enqueue(Oe(n,e,c,o.length,0))},async flush(e){const{ctr:t,hmac:n,pending:s,ready:i}=this;await i;let a=new w;if(s.length){const e=t.update(Le(De,s));n.update(e),a=qe(De,e)}r.signature=qe(De,n.digest()).slice(0,Se),e.enqueue(Ne(a,r.signature))}}),r=this}}function Oe(e,t,n,r,s,i){const{ctr:a,hmac:o,pending:c}=e,l=t.length-s;let u;for(c.length&&(t=Ne(c,t),n=((e,t)=>{if(t&&t>e.length){const n=e;(e=new w(t)).set(n,0)}return e})(n,l-l%we)),u=0;l-we>=u;u+=we){const e=Le(De,We(t,u,u+we));i&&o.update(e);const s=a.update(e);i||o.update(s),n.set(qe(De,s),u+r)}return e.pending=We(t,u),n}async function Ue(e,r,s,i){e.password=null;const a=se(s),o=await(async(e,t,n,r,s)=>{if(!Ie)return ce.importKey(t);try{return await Ce.importKey("raw",t,n,!1,s)}catch(e){return Ie=!1,ce.importKey(t)}})(0,a,pe,0,me),c=await(async(e,t,n)=>{if(!Ae)return ce.pbkdf2(t,e.salt,ge.iterations,n);try{return await Ce.deriveBits(e,t,n)}catch(r){return Ae=!1,ce.pbkdf2(t,e.salt,ge.iterations,n)}})(n.assign({salt:i},ge),o,8*(2*be[r]+2)),l=new w(c),u=Le(De,We(l,0,be[r])),f=Le(De,We(l,be[r],2*be[r])),d=We(l,2*be[r]);return n.assign(e,{keys:{key:u,authentication:f,passwordVerification:d},ctr:new Fe(new Re(u),t.from(ke)),hmac:new Ee(f)}),d}function Ne(e,t){let n=e;return e.length+t.length&&(n=new w(e.length+t.length),n.set(e,0),n.set(t,e.length)),n}function We(e,t,n){return e.subarray(t,n)}function qe(e,t){return e.fromBits(t)}function Le(e,t){return e.toBits(t)}class Me extends v{constructor({password:e,passwordVerification:t}){super({start(){n.assign(this,{password:e,passwordVerification:t}),Ke(this,e)},transform(e,t){const n=this;if(n.password){const t=Ve(n,e.subarray(0,12));if(n.password=null,t[11]!=n.passwordVerification)throw new d(ue);e=e.subarray(12)}t.enqueue(Ve(n,e))}})}}class Pe extends v{constructor({password:e,passwordVerification:t}){super({start(){n.assign(this,{password:e,passwordVerification:t}),Ke(this,e)},transform(e,t){const n=this;let r,s;if(n.password){n.password=null;const t=de(new w(12));t[11]=n.passwordVerification,r=new w(e.length+t.length),r.set(Be(n,t),0),s=12}else r=new w(e.length),s=0;r.set(Be(n,e),s),t.enqueue(r)}})}}function Ve(e,t){const n=new w(t.length);for(let r=0;r>>24]),s=~e.crcKey2.get(),e.keys=[n,r,s]}function Ze(e){const t=2|e.keys[2];return Xe(a.imul(t,1^t)>>>8)}function Xe(e){return 255&e}function Ge(e){return 4294967295&e}const je="deflate-raw";class Je extends v{constructor(e,{chunkSize:t,CompressionStream:n,CompressionStreamNative:r}){super({});const{compressed:s,encrypted:i,useCompressionStream:a,zipCrypto:o,signed:c,level:l}=e,u=this;let f,d,w=$e(super.readable);i&&!o||!c||([w,f]=w.tee(),f=nt(f,new re)),s&&(w=tt(w,a,{level:l,chunkSize:t},r,n)),i&&(o?w=nt(w,new Pe(e)):(d=new He(e),w=nt(w,d))),et(u,w,(async()=>{let e;i&&!o&&(e=d.signature),i&&!o||!c||(e=await f.getReader().read(),e=new g(e.value.buffer).getUint32(0)),u.signature=e}))}}class Qe extends v{constructor(e,{chunkSize:t,DecompressionStream:n,DecompressionStreamNative:r}){super({});const{zipCrypto:s,encrypted:i,signed:a,signature:o,compressed:c,useCompressionStream:l}=e;let u,f,w=$e(super.readable);i&&(s?w=nt(w,new Me(e)):(f=new Te(e),w=nt(w,f))),c&&(w=tt(w,l,{chunkSize:t},r,n)),i&&!s||!a||([w,u]=w.tee(),u=nt(u,new re)),et(this,w,(async()=>{if((!i||s)&&a){const e=await u.getReader().read(),t=new g(e.value.buffer);if(o!=t.getUint32(0,!1))throw new d(fe)}}))}}function $e(e){return nt(e,new v({transform(e,t){e&&e.length&&t.enqueue(e)}}))}function et(e,t,r){t=nt(t,new v({flush:r})),n.defineProperty(e,"readable",{get:()=>t})}function tt(e,t,n,r,s){try{e=nt(e,new(t&&r?r:s)(je,n))}catch(r){if(!t)throw r;e=nt(e,new s(je,n))}return e}function nt(e,t){return e.pipeThrough(t)}const rt="data",st="deflate",it="inflate";class at extends v{constructor(e,t){super({});const r=this,{codecType:s}=e;let i;s.startsWith(st)?i=Je:s.startsWith(it)&&(i=Qe);let a=0;const o=new i(e,t),c=super.readable,l=new v({transform(e,t){e&&e.length&&(a+=e.length,t.enqueue(e))},flush(){const{signature:e}=o;n.assign(r,{signature:e,size:a})}});n.defineProperty(r,"readable",{get:()=>c.pipeThrough(o).pipeThrough(l)})}}const ot=typeof E!=B;class ct{constructor(e,{readable:t,writable:r},{options:s,config:i,streamOptions:a,useWebWorkers:o,transferStreams:c,scripts:l},u){const{signal:f}=a;return n.assign(e,{busy:!0,readable:t.pipeThrough(new lt(t,a,i),{signal:f}),writable:r,options:n.assign({},s),scripts:l,transferStreams:c,terminate(){const{worker:t,busy:n}=e;t&&!n&&(t.terminate(),e.interface=null)},onTaskFinished(){e.busy=!1,u(e)}}),(o&&ot?dt:ft)(e,i)}}class lt extends v{constructor(e,{onstart:t,onprogress:n,size:r,onend:s},{chunkSize:i}){let a=0;super({start(){t&&ut(t,r)},async transform(e,t){a+=e.length,n&&await ut(n,a,r),t.enqueue(e)},flush(){e.size=a,s&&ut(s,a)}},{highWaterMark:1,size:()=>i})}}async function ut(e,...t){try{await e(...t)}catch(e){}}function ft(e,t){return{run:()=>(async({options:e,readable:t,writable:n,onTaskFinished:r},s)=>{const i=new at(e,s);try{await t.pipeThrough(i).pipeTo(n,{preventClose:!0,preventAbort:!0});const{signature:e,size:s}=i;return{signature:e,size:s}}finally{r()}})(e,t)}}function dt(e,{baseURL:t,chunkSize:r}){return e.interface||n.assign(e,{worker:pt(e.scripts[0],t,e),interface:{run:()=>(async(e,t)=>{let r,s;const i=new y(((e,t)=>{r=e,s=t}));n.assign(e,{reader:null,writer:null,resolveResult:r,rejectResult:s,result:i});const{readable:a,options:o,scripts:c}=e,{writable:l,closed:u}=(e=>{const t=e.getWriter();let n;const r=new y((e=>n=e));return{writable:new _({async write(e){await t.ready,await t.write(e)},close(){t.releaseLock(),n()},abort:e=>t.abort(e)}),closed:r}})(e.writable);gt({type:"start",scripts:c.slice(1),options:o,config:t,readable:a,writable:l},e)||n.assign(e,{reader:a.getReader(),writer:l.getWriter()});const f=await i;try{await l.close()}catch(e){}return await u,f})(e,{chunkSize:r})}}),e.interface}let wt=!0,ht=!0;function pt(e,t,r){const s={type:"module"};let i,a;typeof e==K&&(e=e());try{i=new f(e,t)}catch(t){i=e}if(wt)try{a=new E(i)}catch(e){wt=!1,a=new E(i,s)}else a=new E(i,s);return a.addEventListener("message",(e=>(async({data:e},t)=>{const{type:r,value:s,messageId:i,result:a,error:o}=e,{reader:c,writer:l,resolveResult:u,rejectResult:f,onTaskFinished:h}=t;try{if(o){const{message:e,stack:t,code:r,name:s}=o,i=new d(e);n.assign(i,{stack:t,code:r,name:s}),p(i)}else{if("pull"==r){const{value:e,done:n}=await c.read();gt({type:rt,value:e,done:n,messageId:i},t)}r==rt&&(await l.ready,await l.write(new w(s)),gt({type:"ack",messageId:i},t)),"close"==r&&p(null,a)}}catch(o){p(o)}function p(e,t){e?f(e):u(t),l&&l.releaseLock(),h()}})(e,r))),a}function gt(e,{worker:t,writer:n,onTaskFinished:r,transferStreams:s}){try{let{value:n,readable:r,writable:i}=e;const a=[];if(n){const{buffer:t,length:r}=n;r!=t.byteLength&&(n=new w(n)),e.value=n.buffer,a.push(e.value)}if(s&&ht?(r&&a.push(r),i&&a.push(i)):e.readable=e.writable=null,a.length)try{return t.postMessage(e,a),!0}catch(n){ht=!1,e.readable=e.writable=null,t.postMessage(e)}else t.postMessage(e)}catch(e){throw n&&n.releaseLock(),r(),e}}let mt=[];const yt=[];let bt=0;async function St(e,t){const{options:n,config:r}=t,{transferStreams:i,useWebWorkers:a,useCompressionStream:o,codecType:c,compressed:l,signed:u,encrypted:f}=n,{workerScripts:d,maxWorkers:w,terminateWorkerTimeout:h}=r;t.transferStreams=i||i===V;const p=!(l||u||f||t.transferStreams);let g;t.useWebWorkers=!p&&(a||a===V&&r.useWebWorkers),t.scripts=t.useWebWorkers&&d?d[c]:[],n.useCompressionStream=o||o===V&&r.useCompressionStream;const m=mt.find((e=>!e.busy));if(m)kt(m),g=new ct(m,e,t,b);else if(mt.lengthyt.push({resolve:n,stream:e,workerOptions:t})));return g.run();function b(e){if(yt.length){const[{resolve:t,stream:n,workerOptions:r}]=yt.splice(0,1);t(new ct(e,n,r,b))}else e.worker?(kt(e),s.isFinite(h)&&h>=0&&(e.terminateTimeout=setTimeout((()=>{mt=mt.filter((t=>t!=e)),e.terminate()}),h))):mt=mt.filter((t=>t!=e))}}function kt(e){const{terminateTimeout:t}=e;t&&(clearTimeout(t),e.terminateTimeout=null)}const zt="HTTP error ",xt="HTTP Range not supported",vt="Writer iterator completed too soon",Ct="GET",_t=65536,Dt="writable";class Rt{constructor(){this.size=0}init(){this.initialized=!0}}class Ft extends Rt{get readable(){const e=this,{chunkSize:t=_t}=e,n=new C({start(){this.chunkOffset=0},async pull(r){const{offset:s=0,size:i,diskNumberStart:o}=n,{chunkOffset:c}=this;r.enqueue(await Qt(e,s+c,a.min(t,i-c),o)),c+t>i?r.close():this.chunkOffset+=t}});return n}}class Et extends Rt{constructor(){super();const e=this,t=new _({write:t=>e.writeUint8Array(t)});n.defineProperty(e,Dt,{get:()=>t})}writeUint8Array(){}}class It extends Ft{constructor(e){super(),n.assign(this,{blob:e,size:e.size})}async readUint8Array(e,t){const n=this,r=e+t,s=e||rt.writable}),this.blob=new u(t.readable,{headers:r}).blob()}getData(){return this.blob}}class Tt extends Ft{constructor(e,t){super(),Ot(this,e,t)}async init(){super.init(),await Ut(this,Bt,Lt)}readUint8Array(e,t){return Nt(this,e,t,Bt,Lt)}}class Ht extends Ft{constructor(e,t){super(),Ot(this,e,t)}async init(){super.init(),await Ut(this,Kt,Mt)}readUint8Array(e,t){return Nt(this,e,t,Kt,Mt)}}function Ot(e,t,r){const{preventHeadRequest:s,useRangeHeader:i,forceRangeRequests:a}=r;delete(r=n.assign({},r)).preventHeadRequest,delete r.useRangeHeader,delete r.forceRangeRequests,delete r.useXHR,n.assign(e,{url:t,options:r,preventHeadRequest:s,useRangeHeader:i,forceRangeRequests:a})}async function Ut(e,t,n){const{url:r,useRangeHeader:i,forceRangeRequests:a}=e;if((e=>{const{baseURL:t}=j(),{protocol:n}=new f(e,t);return"http:"==n||"https:"==n})(r)&&(i||a)){const{headers:r}=await t(Ct,e,Wt(e));if(!a&&"bytes"!=r.get("Accept-Ranges"))throw new d(xt);{let i;const a=r.get("Content-Range");if(a){const e=a.trim().split(/\s*\/\s*/);if(e.length){const t=e[1];t&&"*"!=t&&(i=s(t))}}i===V?await Vt(e,t,n):e.size=i}}else await Vt(e,t,n)}async function Nt(e,t,n,r,s){const{useRangeHeader:i,forceRangeRequests:a,options:o}=e;if(i||a){const s=await r(Ct,e,Wt(e,t,n));if(206!=s.status)throw new d(xt);return new w(await s.arrayBuffer())}{const{data:r}=e;return r||await s(e,o),new w(e.data.subarray(t,t+n))}}function Wt(e,t=0,r=1){return n.assign({},qt(e),{Range:"bytes="+t+"-"+(t+r-1)})}function qt({options:e}){const{headers:t}=e;if(t)return Symbol.iterator in t?n.fromEntries(t):t}async function Lt(e){await Pt(e,Bt)}async function Mt(e){await Pt(e,Kt)}async function Pt(e,t){const n=await t(Ct,e,qt(e));e.data=new w(await n.arrayBuffer()),e.size||(e.size=e.data.length)}async function Vt(e,t,n){if(e.preventHeadRequest)await n(e,e.options);else{const r=(await t("HEAD",e,qt(e))).headers.get("Content-Length");r?e.size=s(r):await n(e,e.options)}}async function Bt(e,{options:t,url:r},s){const i=await fetch(r,n.assign({},t,{method:e,headers:s}));if(400>i.status)return i;throw 416==i.status?new d(xt):new d(zt+(i.statusText||i.status))}function Kt(e,{url:t},r){return new y(((s,i)=>{const a=new XMLHttpRequest;if(a.addEventListener("load",(()=>{if(400>a.status){const e=[];a.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach((t=>{const n=t.trim().split(/\s*:\s*/);n[0]=n[0].trim().replace(/^[a-z]|-[a-z]/g,(e=>e.toUpperCase())),e.push(n)})),s({status:a.status,arrayBuffer:()=>a.response,headers:new c(e)})}else i(416==a.status?new d(xt):new d(zt+(a.statusText||a.status)))}),!1),a.addEventListener("error",(e=>i(e.detail.error)),!1),a.open(e,t),r)for(const e of n.entries(r))a.setRequestHeader(e[0],e[1]);a.responseType="arraybuffer",a.send()}))}class Yt extends Ft{constructor(e,t={}){super(),n.assign(this,{url:e,reader:t.useXHR?new Ht(e,t):new Tt(e,t)})}set size(e){}get size(){return this.reader.size}async init(){super.init(),await this.reader.init()}readUint8Array(e,t){return this.reader.readUint8Array(e,t)}}class Zt extends Ft{constructor(e){super(),this.readers=e}async init(){super.init();const e=this,{readers:t}=e;e.lastDiskNumber=0,await y.all(t.map((async t=>{await t.init(),e.size+=t.size})))}async readUint8Array(e,t,n=0){const r=this,{readers:s}=this;let i,o=n;-1==o&&(o=s.length-1);let c=e;for(;c>=s[o].size;)c-=s[o].size,o++;const l=s[o],u=l.size;if(c+t>u){const s=u-c;i=new w(t),i.set(await Qt(l,c,s)),i.set(await r.readUint8Array(e+s,t-s,n),s)}else i=await Qt(l,c,t);return r.lastDiskNumber=a.max(o,r.lastDiskNumber),i}}class Xt extends Rt{constructor(e,t=4294967295){super();const r=this;let s,i,a;n.assign(r,{diskNumber:0,diskOffset:0,size:0,maxSize:t,availableSize:t});const o=new _({async write(t){const{availableSize:n}=r;if(a)t.lengtho})}}async function Gt(e,t){e.init&&!e.initialized&&await e.init(t)}function jt(e){return t.isArray(e)&&(e=new Zt(e)),e instanceof C&&(e={readable:e}),e}function Jt(e){e.writable===V&&typeof e.next==K&&(e=new Xt(e)),e instanceof _&&(e={writable:e});const{writable:t}=e;return t.size===V&&(t.size=0),e instanceof Xt||n.assign(e,{diskNumber:0,diskOffset:0,availableSize:1/0,maxSize:1/0}),e}function Qt(e,t,n,r){return e.readUint8Array(t,n,r)}const $t=Zt,en=Xt,tn="\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ".split("");function nn(e,t){return t&&"cp437"==t.trim().toLowerCase()?(e=>{{let t="";for(let n=0;nthis[t]=e[t]))}}const zn="File format is not recognized",xn="End of central directory not found",vn="End of Zip64 central directory not found",Cn="End of Zip64 central directory locator not found",_n="Central directory header not found",Dn="Local file header not found",Rn="Zip64 extra field not found",Fn="File contains encrypted entry",En="Encryption method not supported",In="Compression method not supported",An="Split zip file",Tn="utf-8",Hn="cp437",On=[[cn,I],[ln,I],[un,I],[fn,A]],Un={[A]:{getValue:Yn,bytes:4},[I]:{getValue:Zn,bytes:8}};class Nn{constructor(e,t,r){n.assign(this,{reader:e,config:t,options:r})}async getData(e,t,r={}){const s=this,{reader:i,offset:a,diskNumberStart:o,extraFieldAES:c,compressionMethod:l,config:u,bitFlag:f,signature:h,rawLastModDate:p,uncompressedSize:g,compressedSize:m}=s,y=s.localDirectory={},b=Xn(await Qt(i,a,30,o));let S=Mn(s,r,"password");if(S=S&&S.length&&S,c&&99!=c.originalCompressionMethod)throw new d(In);if(0!=l&&8!=l)throw new d(In);if(Yn(b,0)!=T)throw new d(Dn);Wn(y,b,4),y.rawExtraField=y.extraFieldLength?await Qt(i,a+30+y.filenameLength,y.extraFieldLength,o):new w,await qn(s,y,b,4),n.assign(t,{lastAccessDate:y.lastAccessDate,creationDate:y.creationDate});const k=s.encrypted&&y.encrypted,z=k&&!c;if(k){if(!z&&c.strength===V)throw new d(En);if(!S)throw new d(Fn)}const x=a+30+y.filenameLength+y.extraFieldLength,v=i.readable;v.diskNumberStart=o,v.offset=x;const C=v.size=m,_=Mn(s,r,"signal");e=Jt(e),await Gt(e,g);const{writable:D}=e,{onstart:R,onprogress:F,onend:E}=r,I={options:{codecType:it,password:S,zipCrypto:z,encryptionStrength:c&&c.strength,signed:Mn(s,r,"checkSignature"),passwordVerification:z&&(f.dataDescriptor?p>>>8&255:h>>>24&255),signature:h,compressed:0!=l,encrypted:k,useWebWorkers:Mn(s,r,"useWebWorkers"),useCompressionStream:Mn(s,r,"useCompressionStream"),transferStreams:Mn(s,r,"transferStreams")},config:u,streamOptions:{signal:_,size:C,onstart:R,onprogress:F,onend:E}};return D.size+=(await St({readable:v,writable:D},I)).size,Mn(s,r,"preventClose")||await D.close(),e.getData?e.getData():D}}function Wn(e,t,r){const s=e.rawBitFlag=Kn(t,r+2),i=1==(1&s),a=Yn(t,r+6);n.assign(e,{encrypted:i,version:Kn(t,r),bitFlag:{level:(6&s)>>1,dataDescriptor:8==(8&s),languageEncodingFlag:(s&q)==q},rawLastModDate:a,lastModDate:Pn(a),filenameLength:Kn(t,r+22),extraFieldLength:Kn(t,r+24)})}async function qn(e,t,r,s){const{rawExtraField:i}=t,a=t.extraField=new c,l=Xn(new w(i));let u=0;try{for(;u{t.zip64=!0;const n=Xn(e.data),r=On.filter((([e,n])=>t[e]==n));for(let s=0,i=0;s{const s=Xn(e.data),i=Bn(s,4);n.assign(e,{vendorVersion:Bn(s,0),vendorId:Bn(s,2),strength:i,originalCompressionMethod:r,compressionMethod:Kn(s,5)}),t.compressionMethod=e.compressionMethod})(m,t,f),t.extraFieldAES=m):t.compressionMethod=f;const y=a.get(10);y&&(((e,t)=>{const r=Xn(e.data);let s,i=4;try{for(;i{const n=Xn(e.data),r=Bn(n,0),s=[],i=[];1==(1&r)&&(s.push(dn),i.push(wn)),2==(2&r)&&(s.push(hn),i.push("rawLastAccessDate")),4==(4&r)&&(s.push(pn),i.push("rawCreationDate"));let a=1;s.forEach(((r,s)=>{if(e.data.length>=a+4){const c=Yn(n,a);t[r]=e[r]=new o(1e3*c);const l=i[s];e[l]=c}a+=4}))})(b,t),t.extraFieldExtendedTimestamp=b)}async function Ln(e,t,r,s,i){const a=Xn(e.data),o=new ne;o.append(i[r]);const c=Xn(new w(4));c.setUint32(0,o.get(),!0),n.assign(e,{version:Bn(a,0),signature:Yn(a,1),[t]:await nn(e.data.subarray(5)),valid:!i.bitFlag.languageEncodingFlag&&e.signature==Yn(c,0)}),e.valid&&(s[t]=e[t],s[t+"UTF8"]=!0)}function Mn(e,t,n){return t[n]===V?e.options[n]:t[n]}function Pn(e){const t=(4294901760&e)>>16,n=65535&e;try{return new o(1980+((65024&t)>>9),((480&t)>>5)-1,31&t,(63488&n)>>11,(2016&n)>>5,2*(31&n),0)}catch(e){}}function Vn(e){return new o(s(e/i(1e4)-i(116444736e5)))}function Bn(e,t){return e.getUint8(t)}function Kn(e,t){return e.getUint16(t,!0)}function Yn(e,t){return e.getUint32(t,!0)}function Zn(e,t){return s(e.getBigUint64(t,!0))}function Xn(e){return new g(e.buffer)}const Gn="File already exists",jn="Zip file comment exceeds 64KB",Jn="File entry comment exceeds 64KB",Qn="File entry name exceeds 64KB",$n="Version exceeds 65535",er="The strength must equal 1, 2, or 3",tr="Extra field type exceeds 65535",nr="Extra field data exceeds 64KB",rr="Zip64 is not supported (make sure 'keepOrder' is set to 'true')",sr=new w([7,0,2,0,65,69,3,0,0]);let ir=0;const ar=[];async function or(e,t){const n=e.getWriter();await n.ready,e.size+=gr(t),await n.write(t),n.releaseLock()}function cr(e){if(e)return(i(e.getTime())+i(116444736e5))*i(1e4)}function lr(e,t,n,r){const s=t[n]===V?e.options[n]:t[n];return s===V?r:s}function ur(e,t,n){e.setUint8(t,n)}function fr(e,t,n){e.setUint16(t,n,!0)}function dr(e,t,n){e.setUint32(t,n,!0)}function wr(e,t,n){e.setBigUint64(t,n,!0)}function hr(e,t,n){e.set(t,n)}function pr(e){return new g(e.buffer)}function gr(...e){let t=0;return e.forEach((e=>e&&(t+=e.length))),t}let mr;try{mr=void 0===k&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:void 0===k?location.href:k.currentScript&&k.currentScript.src||new f("zip.min.js",k.baseURI).href}catch(e){}Q({baseURL:mr}),(e=>{const t=()=>f.createObjectURL(new m(['const{Array:e,Object:t,Number:n,Math:r,Error:s,Uint8Array:i,Uint16Array:o,Uint32Array:c,Int32Array:f,Map:a,DataView:l,Promise:u,TextEncoder:w,crypto:h,postMessage:d,TransformStream:p,ReadableStream:y,WritableStream:m,CompressionStream:b,DecompressionStream:g}=self;class k{constructor(e){return class extends p{constructor(t,n){const r=new e(n);super({transform(e,t){t.enqueue(r.append(e))},flush(e){const t=r.flush();t&&e.enqueue(t)}})}}}}const v=[];for(let e=0;256>e;e++){let t=e;for(let e=0;8>e;e++)1&t?t=t>>>1^3988292384:t>>>=1;v[e]=t}class S{constructor(e){this.t=e||-1}append(e){let t=0|this.t;for(let n=0,r=0|e.length;r>n;n++)t=t>>>8^v[255&(t^e[n])];this.t=t}get(){return~this.t}}class z extends p{constructor(){const e=new S;super({transform(t){e.append(t)},flush(t){const n=new i(4);new l(n.buffer).setUint32(0,e.get()),t.enqueue(n)}})}}const C={concat(e,t){if(0===e.length||0===t.length)return e.concat(t);const n=e[e.length-1],r=C.i(n);return 32===r?e.concat(t):C.o(t,r,0|n,e.slice(0,e.length-1))},l(e){const t=e.length;if(0===t)return 0;const n=e[t-1];return 32*(t-1)+C.i(n)},u(e,t){if(32*e.length0&&t&&(e[n-1]=C.h(t,e[n-1]&2147483648>>t-1,1)),e},h:(e,t,n)=>32===e?t:(n?0|t:t<<32-e)+1099511627776*e,i:e=>r.round(e/1099511627776)||32,o(e,t,n,r){for(void 0===r&&(r=[]);t>=32;t-=32)r.push(n),n=0;if(0===t)return r.concat(e);for(let s=0;s>>t),n=e[s]<<32-t;const s=e.length?e[e.length-1]:0,i=C.i(s);return r.push(C.h(t+i&31,t+i>32?n:r.pop(),1)),r}},I={p:{m(e){const t=C.l(e)/8,n=new i(t);let r;for(let s=0;t>s;s++)0==(3&s)&&(r=e[s/4]),n[s]=r>>>24,r<<=8;return n},g(e){const t=[];let n,r=0;for(n=0;n{let t=987654321;const n=4294967295;return()=>(t=36969*(65535&t)+(t>>16)&n,(((t<<16)+(e=18e3*(65535&e)+(e>>16)&n)&n)/4294967296+.5)*(r.random()>.5?1:-1))};for(let s,i=0;inew _.k(I.p.g(e)),v(e,t,n,r){if(n=n||1e4,0>r||0>n)throw new s("invalid params to pbkdf2");const i=1+(r>>5)<<2;let o,c,f,a,u;const w=new ArrayBuffer(i),h=new l(w);let d=0;const p=C;for(t=I.p.g(t),u=1;(i||1)>d;u++){for(o=c=e.encrypt(p.concat(t,[u])),f=1;n>f;f++)for(c=e.encrypt(c),a=0;ad&&f9007199254740991)throw new s("Cannot hash more than 2^53 - 1 bits");const o=new c(n);let f=0;for(let e=t.blockSize+r-(t.blockSize+r&t.blockSize-1);i>=e;e+=t.blockSize)t.R(o.subarray(16*f,16*(f+1))),f+=1;return n.splice(0,16*f),t}B(){const e=this;let t=e.A;const n=e._;t=C.concat(t,[C.h(1,1)]);for(let e=t.length+2;15&e;e++)t.push(0);for(t.push(r.floor(e.D/4294967296)),t.push(0|e.D);t.length;)e.R(t.splice(0,16));return e.reset(),n}M(e,t,n,r){return e>19?e>39?e>59?e>79?void 0:t^n^r:t&n|t&r|n&r:t^n^r:t&n|~t&r}K(e,t){return t<>>32-e}R(t){const n=this,s=n._,i=e(80);for(let e=0;16>e;e++)i[e]=t[e];let o=s[0],c=s[1],f=s[2],a=s[3],l=s[4];for(let e=0;79>=e;e++){16>e||(i[e]=n.K(1,i[e-3]^i[e-8]^i[e-14]^i[e-16]));const t=n.K(5,o)+n.M(e,c,f,a)+l+i[e]+n.I[r.floor(e/20)]|0;l=a,a=f,f=n.K(30,c),c=o,o=t}s[0]=s[0]+o|0,s[1]=s[1]+c|0,s[2]=s[2]+f|0,s[3]=s[3]+a|0,s[4]=s[4]+l|0}},o=[[],[]];n.P=[new i,new i];const f=n.P[0].blockSize/32;t.length>f&&(t=i.hash(t));for(let e=0;f>e;e++)o[0][e]=909522486^t[e],o[1][e]=1549556828^t[e];n.P[0].update(o[0]),n.P[1].update(o[1]),n.U=new i(n.P[0])}reset(){const e=this;e.U=new e.S(e.P[0]),e.N=!1}update(e){this.N=!0,this.U.update(e)}digest(){const e=this,t=e.U.B(),n=new e.S(e.P[1]).update(t).B();return e.reset(),n}encrypt(e){if(this.N)throw new s("encrypt on already updated hmac called!");return this.update(e),this.digest(e)}}},A=void 0!==h&&"function"==typeof h.getRandomValues;function D(e){return A?h.getRandomValues(e):x.getRandomValues(e)}const V={name:"PBKDF2"},R=t.assign({hash:{name:"HMAC"}},V),B=t.assign({iterations:1e3,hash:{name:"SHA-1"}},V),E=["deriveBits"],M=[8,12,16],K=[16,24,32],P=[0,0,0,0],U=void 0!==h,N=U&&h.subtle,T=U&&void 0!==N,W=I.p,H=class{constructor(e){const t=this;t.T=[[[],[],[],[],[]],[[],[],[],[],[]]],t.T[0][0][0]||t.W();const n=t.T[0][4],r=t.T[1],i=e.length;let o,c,f,a=1;if(4!==i&&6!==i&&8!==i)throw new s("invalid aes key size");for(t.I=[c=e.slice(0),f=[]],o=i;4*i+28>o;o++){let e=c[o-1];(o%i==0||8===i&&o%i==4)&&(e=n[e>>>24]<<24^n[e>>16&255]<<16^n[e>>8&255]<<8^n[255&e],o%i==0&&(e=e<<8^e>>>24^a<<24,a=a<<1^283*(a>>7))),c[o]=c[o-i]^e}for(let e=0;o;e++,o--){const t=c[3&e?o:o-4];f[e]=4>=o||4>e?t:r[0][n[t>>>24]]^r[1][n[t>>16&255]]^r[2][n[t>>8&255]]^r[3][n[255&t]]}}encrypt(e){return this.H(e,0)}decrypt(e){return this.H(e,1)}W(){const e=this.T[0],t=this.T[1],n=e[4],r=t[4],s=[],i=[];let o,c,f,a;for(let e=0;256>e;e++)i[(s[e]=e<<1^283*(e>>7))^e]=e;for(let l=o=0;!n[l];l^=c||1,o=i[o]||1){let i=o^o<<1^o<<2^o<<3^o<<4;i=i>>8^255&i^99,n[l]=i,r[i]=l,a=s[f=s[c=s[l]]];let u=16843009*a^65537*f^257*c^16843008*l,w=257*s[i]^16843008*i;for(let n=0;4>n;n++)e[n][l]=w=w<<24^w>>>8,t[n][i]=u=u<<24^u>>>8}for(let n=0;5>n;n++)e[n]=e[n].slice(0),t[n]=t[n].slice(0)}H(e,t){if(4!==e.length)throw new s("invalid aes block size");const n=this.I[t],r=n.length/4-2,i=[0,0,0,0],o=this.T[t],c=o[0],f=o[1],a=o[2],l=o[3],u=o[4];let w,h,d,p=e[0]^n[0],y=e[t?3:1]^n[1],m=e[2]^n[2],b=e[t?1:3]^n[3],g=4;for(let e=0;r>e;e++)w=c[p>>>24]^f[y>>16&255]^a[m>>8&255]^l[255&b]^n[g],h=c[y>>>24]^f[m>>16&255]^a[b>>8&255]^l[255&p]^n[g+1],d=c[m>>>24]^f[b>>16&255]^a[p>>8&255]^l[255&y]^n[g+2],b=c[b>>>24]^f[p>>16&255]^a[y>>8&255]^l[255&m]^n[g+3],g+=4,p=w,y=h,m=d;for(let e=0;4>e;e++)i[t?3&-e:e]=u[p>>>24]<<24^u[y>>16&255]<<16^u[m>>8&255]<<8^u[255&b]^n[g++],w=p,p=y,y=m,m=b,b=w;return i}},L=class{constructor(e,t){this.L=e,this.j=t,this.F=t}reset(){this.F=this.j}update(e){return this.O(this.L,e,this.F)}q(e){if(255==(e>>24&255)){let t=e>>16&255,n=e>>8&255,r=255&e;255===t?(t=0,255===n?(n=0,255===r?r=0:++r):++n):++t,e=0,e+=t<<16,e+=n<<8,e+=r}else e+=1<<24;return e}G(e){0===(e[0]=this.q(e[0]))&&(e[1]=this.q(e[1]))}O(e,t,n){let r;if(!(r=t.length))return[];const s=C.l(t);for(let s=0;r>s;s+=4){this.G(n);const r=e.encrypt(n);t[s]^=r[0],t[s+1]^=r[1],t[s+2]^=r[2],t[s+3]^=r[3]}return C.u(t,s)}},j=_.k;let F=U&&T&&"function"==typeof N.importKey,O=U&&T&&"function"==typeof N.deriveBits;class q extends p{constructor({password:e,signed:n,encryptionStrength:r}){super({start(){t.assign(this,{ready:new u((e=>this.J=e)),password:e,signed:n,X:r-1,pending:new i})},async transform(e,t){const n=this,{password:r,X:o,J:c,ready:f}=n;r?(await(async(e,t,n,r)=>{const i=await Q(e,t,n,Y(r,0,M[t])),o=Y(r,M[t]);if(i[0]!=o[0]||i[1]!=o[1])throw new s("Invalid password")})(n,o,r,Y(e,0,M[o]+2)),e=Y(e,M[o]+2),c()):await f;const a=new i(e.length-10-(e.length-10)%16);t.enqueue(J(n,e,a,0,10,!0))},async flush(e){const{signed:t,Y:n,Z:r,pending:o,ready:c}=this;await c;const f=Y(o,0,o.length-10),a=Y(o,o.length-10);let l=new i;if(f.length){const e=$(W,f);r.update(e);const t=n.update(e);l=Z(W,t)}if(t){const e=Y(Z(W,r.digest()),0,10);for(let t=0;10>t;t++)if(e[t]!=a[t])throw new s("Invalid signature")}e.enqueue(l)}})}}class G extends p{constructor({password:e,encryptionStrength:n}){let r;super({start(){t.assign(this,{ready:new u((e=>this.J=e)),password:e,X:n-1,pending:new i})},async transform(e,t){const n=this,{password:r,X:s,J:o,ready:c}=n;let f=new i;r?(f=await(async(e,t,n)=>{const r=D(new i(M[t]));return X(r,await Q(e,t,n,r))})(n,s,r),o()):await c;const a=new i(f.length+e.length-e.length%16);a.set(f,0),t.enqueue(J(n,e,a,f.length,0))},async flush(e){const{Y:t,Z:n,pending:s,ready:o}=this;await o;let c=new i;if(s.length){const e=t.update($(W,s));n.update(e),c=Z(W,e)}r.signature=Z(W,n.digest()).slice(0,10),e.enqueue(X(c,r.signature))}}),r=this}}function J(e,t,n,r,s,o){const{Y:c,Z:f,pending:a}=e,l=t.length-s;let u;for(a.length&&(t=X(a,t),n=((e,t)=>{if(t&&t>e.length){const n=e;(e=new i(t)).set(n,0)}return e})(n,l-l%16)),u=0;l-16>=u;u+=16){const e=$(W,Y(t,u,u+16));o&&f.update(e);const s=c.update(e);o||f.update(s),n.set(Z(W,s),u+r)}return e.pending=Y(t,u),n}async function Q(n,r,s,o){n.password=null;const c=(e=>{if(void 0===w){const t=new i((e=unescape(encodeURIComponent(e))).length);for(let n=0;n{if(!F)return _.importKey(t);try{return await N.importKey("raw",t,n,!1,s)}catch(e){return F=!1,_.importKey(t)}})(0,c,R,0,E),a=await(async(e,t,n)=>{if(!O)return _.v(t,e.salt,B.iterations,n);try{return await N.deriveBits(e,t,n)}catch(r){return O=!1,_.v(t,e.salt,B.iterations,n)}})(t.assign({salt:o},B),f,8*(2*K[r]+2)),l=new i(a),u=$(W,Y(l,0,K[r])),h=$(W,Y(l,K[r],2*K[r])),d=Y(l,2*K[r]);return t.assign(n,{keys:{key:u,$:h,passwordVerification:d},Y:new L(new H(u),e.from(P)),Z:new j(h)}),d}function X(e,t){let n=e;return e.length+t.length&&(n=new i(e.length+t.length),n.set(e,0),n.set(t,e.length)),n}function Y(e,t,n){return e.subarray(t,n)}function Z(e,t){return e.m(t)}function $(e,t){return e.g(t)}class ee extends p{constructor({password:e,passwordVerification:n}){super({start(){t.assign(this,{password:e,passwordVerification:n}),se(this,e)},transform(e,t){const n=this;if(n.password){const t=ne(n,e.subarray(0,12));if(n.password=null,t[11]!=n.passwordVerification)throw new s("Invalid password");e=e.subarray(12)}t.enqueue(ne(n,e))}})}}class te extends p{constructor({password:e,passwordVerification:n}){super({start(){t.assign(this,{password:e,passwordVerification:n}),se(this,e)},transform(e,t){const n=this;let r,s;if(n.password){n.password=null;const t=D(new i(12));t[11]=n.passwordVerification,r=new i(e.length+t.length),r.set(re(n,t),0),s=12}else r=new i(e.length),s=0;r.set(re(n,e),s),t.enqueue(r)}})}}function ne(e,t){const n=new i(t.length);for(let r=0;r>>24]),i=~e.te.get(),e.keys=[n,s,i]}function oe(e){const t=2|e.keys[2];return ce(r.imul(t,1^t)>>>8)}function ce(e){return 255&e}function fe(e){return 4294967295&e}class ae extends p{constructor(e,{chunkSize:t,CompressionStream:n,CompressionStreamNative:r}){super({});const{compressed:s,encrypted:i,useCompressionStream:o,zipCrypto:c,signed:f,level:a}=e,u=this;let w,h,d=ue(super.readable);i&&!c||!f||([d,w]=d.tee(),w=de(w,new z)),s&&(d=he(d,o,{level:a,chunkSize:t},r,n)),i&&(c?d=de(d,new te(e)):(h=new G(e),d=de(d,h))),we(u,d,(async()=>{let e;i&&!c&&(e=h.signature),i&&!c||!f||(e=await w.getReader().read(),e=new l(e.value.buffer).getUint32(0)),u.signature=e}))}}class le extends p{constructor(e,{chunkSize:t,DecompressionStream:n,DecompressionStreamNative:r}){super({});const{zipCrypto:i,encrypted:o,signed:c,signature:f,compressed:a,useCompressionStream:u}=e;let w,h,d=ue(super.readable);o&&(i?d=de(d,new ee(e)):(h=new q(e),d=de(d,h))),a&&(d=he(d,u,{chunkSize:t},r,n)),o&&!i||!c||([d,w]=d.tee(),w=de(w,new z)),we(this,d,(async()=>{if((!o||i)&&c){const e=await w.getReader().read(),t=new l(e.value.buffer);if(f!=t.getUint32(0,!1))throw new s("Invalid signature")}}))}}function ue(e){return de(e,new p({transform(e,t){e&&e.length&&t.enqueue(e)}}))}function we(e,n,r){n=de(n,new p({flush:r})),t.defineProperty(e,"readable",{get:()=>n})}function he(e,t,n,r,s){try{e=de(e,new(t&&r?r:s)("deflate-raw",n))}catch(r){if(!t)throw r;e=de(e,new s("deflate-raw",n))}return e}function de(e,t){return e.pipeThrough(t)}class pe extends p{constructor(e,n){super({});const r=this,{codecType:s}=e;let i;s.startsWith("deflate")?i=ae:s.startsWith("inflate")&&(i=le);let o=0;const c=new i(e,n),f=super.readable,a=new p({transform(e,t){e&&e.length&&(o+=e.length,t.enqueue(e))},flush(){const{signature:e}=c;t.assign(r,{signature:e,size:o})}});t.defineProperty(r,"readable",{get:()=>f.pipeThrough(c).pipeThrough(a)})}}const ye=new a,me=new a;let be=0;async function ge(e){try{const{options:t,scripts:r,config:s}=e;r&&r.length&&importScripts.apply(void 0,r),self.initCodec&&self.initCodec(),s.CompressionStreamNative=self.CompressionStream,s.DecompressionStreamNative=self.DecompressionStream,self.Deflate&&(s.CompressionStream=new k(self.Deflate)),self.Inflate&&(s.DecompressionStream=new k(self.Inflate));const i={highWaterMark:1,size:()=>s.chunkSize},o=e.readable||new y({async pull(e){const t=new u((e=>ye.set(be,e)));ke({type:"pull",messageId:be}),be=(be+1)%n.MAX_SAFE_INTEGER;const{value:r,done:s}=await t;e.enqueue(r),s&&e.close()}},i),c=e.writable||new m({async write(e){let t;const r=new u((e=>t=e));me.set(be,t),ke({type:"data",value:e,messageId:be}),be=(be+1)%n.MAX_SAFE_INTEGER,await r}},i),f=new pe(t,s);await o.pipeThrough(f).pipeTo(c,{preventAbort:!0});try{await c.close()}catch(e){}const{signature:a,size:l}=f;ke({type:"close",result:{signature:a,size:l}})}catch(e){ve(e)}}function ke(e){let{value:t}=e;if(t)if(t.length)try{t=new i(t),e.value=t.buffer,d(e,[e.value])}catch(t){d(e)}else d(e);else d(e)}function ve(e){const{message:t,stack:n,code:r,name:s}=e;d({error:{message:t,stack:n,code:r,name:s}})}function Se(t){return ze(t.map((([t,n])=>new e(t).fill(n,0,t))))}function ze(t){return t.reduce(((t,n)=>t.concat(e.isArray(n)?ze(n):n)),[])}addEventListener("message",(({data:e})=>{const{type:t,messageId:n,value:r,done:s}=e;try{if("start"==t&&ge(e),"data"==t){const e=ye.get(n);ye.delete(n),e({value:new i(r),done:s})}if("ack"==t){const e=me.get(n);me.delete(n),e()}}catch(e){ve(e)}}));const Ce=[0,1,2,3].concat(...Se([[2,4],[2,5],[4,6],[4,7],[8,8],[8,9],[16,10],[16,11],[32,12],[32,13],[64,14],[64,15],[2,0],[1,16],[1,17],[2,18],[2,19],[4,20],[4,21],[8,22],[8,23],[16,24],[16,25],[32,26],[32,27],[64,28],[64,29]]));function Ie(){const e=this;function t(e,t){let n=0;do{n|=1&e,e>>>=1,n<<=1}while(--t>0);return n>>>1}e.ne=n=>{const s=e.re,i=e.ie.se,o=e.ie.oe;let c,f,a,l=-1;for(n.ce=0,n.fe=573,c=0;o>c;c++)0!==s[2*c]?(n.ae[++n.ce]=l=c,n.le[c]=0):s[2*c+1]=0;for(;2>n.ce;)a=n.ae[++n.ce]=2>l?++l:0,s[2*a]=1,n.le[a]=0,n.ue--,i&&(n.we-=i[2*a+1]);for(e.he=l,c=r.floor(n.ce/2);c>=1;c--)n.de(s,c);a=o;do{c=n.ae[1],n.ae[1]=n.ae[n.ce--],n.de(s,1),f=n.ae[1],n.ae[--n.fe]=c,n.ae[--n.fe]=f,s[2*a]=s[2*c]+s[2*f],n.le[a]=r.max(n.le[c],n.le[f])+1,s[2*c+1]=s[2*f+1]=a,n.ae[1]=a++,n.de(s,1)}while(n.ce>=2);n.ae[--n.fe]=n.ae[1],(t=>{const n=e.re,r=e.ie.se,s=e.ie.pe,i=e.ie.ye,o=e.ie.me;let c,f,a,l,u,w,h=0;for(l=0;15>=l;l++)t.be[l]=0;for(n[2*t.ae[t.fe]+1]=0,c=t.fe+1;573>c;c++)f=t.ae[c],l=n[2*n[2*f+1]+1]+1,l>o&&(l=o,h++),n[2*f+1]=l,f>e.he||(t.be[l]++,u=0,i>f||(u=s[f-i]),w=n[2*f],t.ue+=w*(l+u),r&&(t.we+=w*(r[2*f+1]+u)));if(0!==h){do{for(l=o-1;0===t.be[l];)l--;t.be[l]--,t.be[l+1]+=2,t.be[o]--,h-=2}while(h>0);for(l=o;0!==l;l--)for(f=t.be[l];0!==f;)a=t.ae[--c],a>e.he||(n[2*a+1]!=l&&(t.ue+=(l-n[2*a+1])*n[2*a],n[2*a+1]=l),f--)}})(n),((e,n,r)=>{const s=[];let i,o,c,f=0;for(i=1;15>=i;i++)s[i]=f=f+r[i-1]<<1;for(o=0;n>=o;o++)c=e[2*o+1],0!==c&&(e[2*o]=t(s[c]++,c))})(s,e.he,n.be)}}function xe(e,t,n,r,s){const i=this;i.se=e,i.pe=t,i.ye=n,i.oe=r,i.me=s}Ie.ge=[0,1,2,3,4,5,6,7].concat(...Se([[2,8],[2,9],[2,10],[2,11],[4,12],[4,13],[4,14],[4,15],[8,16],[8,17],[8,18],[8,19],[16,20],[16,21],[16,22],[16,23],[32,24],[32,25],[32,26],[31,27],[1,28]])),Ie.ke=[0,1,2,3,4,5,6,7,8,10,12,14,16,20,24,28,32,40,48,56,64,80,96,112,128,160,192,224,0],Ie.ve=[0,1,2,3,4,6,8,12,16,24,32,48,64,96,128,192,256,384,512,768,1024,1536,2048,3072,4096,6144,8192,12288,16384,24576],Ie.Se=e=>256>e?Ce[e]:Ce[256+(e>>>7)],Ie.ze=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],Ie.Ce=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Ie.Ie=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],Ie.xe=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];const _e=Se([[144,8],[112,9],[24,7],[8,8]]);xe._e=ze([12,140,76,204,44,172,108,236,28,156,92,220,60,188,124,252,2,130,66,194,34,162,98,226,18,146,82,210,50,178,114,242,10,138,74,202,42,170,106,234,26,154,90,218,58,186,122,250,6,134,70,198,38,166,102,230,22,150,86,214,54,182,118,246,14,142,78,206,46,174,110,238,30,158,94,222,62,190,126,254,1,129,65,193,33,161,97,225,17,145,81,209,49,177,113,241,9,137,73,201,41,169,105,233,25,153,89,217,57,185,121,249,5,133,69,197,37,165,101,229,21,149,85,213,53,181,117,245,13,141,77,205,45,173,109,237,29,157,93,221,61,189,125,253,19,275,147,403,83,339,211,467,51,307,179,435,115,371,243,499,11,267,139,395,75,331,203,459,43,299,171,427,107,363,235,491,27,283,155,411,91,347,219,475,59,315,187,443,123,379,251,507,7,263,135,391,71,327,199,455,39,295,167,423,103,359,231,487,23,279,151,407,87,343,215,471,55,311,183,439,119,375,247,503,15,271,143,399,79,335,207,463,47,303,175,431,111,367,239,495,31,287,159,415,95,351,223,479,63,319,191,447,127,383,255,511,0,64,32,96,16,80,48,112,8,72,40,104,24,88,56,120,4,68,36,100,20,84,52,116,3,131,67,195,35,163,99,227].map(((e,t)=>[e,_e[t]])));const Ae=Se([[30,5]]);function De(e,t,n,r,s){const i=this;i.Ae=e,i.De=t,i.Ve=n,i.Re=r,i.Be=s}xe.Ee=ze([0,16,8,24,4,20,12,28,2,18,10,26,6,22,14,30,1,17,9,25,5,21,13,29,3,19,11,27,7,23].map(((e,t)=>[e,Ae[t]]))),xe.Me=new xe(xe._e,Ie.ze,257,286,15),xe.Ke=new xe(xe.Ee,Ie.Ce,0,30,15),xe.Pe=new xe(null,Ie.Ie,0,19,7);const Ve=[new De(0,0,0,0,0),new De(4,4,8,4,1),new De(4,5,16,8,1),new De(4,6,32,32,1),new De(4,4,16,16,2),new De(8,16,32,32,2),new De(8,16,128,128,2),new De(8,32,128,256,2),new De(32,128,258,1024,2),new De(32,258,258,4096,2)],Re=["need dictionary","stream end","","","stream error","data error","","buffer error","",""];function Be(e,t,n,r){const s=e[2*t],i=e[2*n];return i>s||s==i&&r[t]<=r[n]}function Ee(){const e=this;let t,n,s,c,f,a,l,u,w,h,d,p,y,m,b,g,k,v,S,z,C,I,x,_,A,D,V,R,B,E,M,K,P;const U=new Ie,N=new Ie,T=new Ie;let W,H,L,j,F,O;function q(){let t;for(t=0;286>t;t++)M[2*t]=0;for(t=0;30>t;t++)K[2*t]=0;for(t=0;19>t;t++)P[2*t]=0;M[512]=1,e.ue=e.we=0,H=L=0}function G(e,t){let n,r=-1,s=e[1],i=0,o=7,c=4;0===s&&(o=138,c=3),e[2*(t+1)+1]=65535;for(let f=0;t>=f;f++)n=s,s=e[2*(f+1)+1],++ii?P[2*n]+=i:0!==n?(n!=r&&P[2*n]++,P[32]++):i>10?P[36]++:P[34]++,i=0,r=n,0===s?(o=138,c=3):n==s?(o=6,c=3):(o=7,c=4))}function J(t){e.Ue[e.pending++]=t}function Q(e){J(255&e),J(e>>>8&255)}function X(e,t){let n;const r=t;O>16-r?(n=e,F|=n<>>16-O,O+=r-16):(F|=e<=n;n++)if(r=i,i=e[2*(n+1)+1],++o>=c||r!=i){if(f>o)do{Y(r,P)}while(0!=--o);else 0!==r?(r!=s&&(Y(r,P),o--),Y(16,P),X(o-3,2)):o>10?(Y(18,P),X(o-11,7)):(Y(17,P),X(o-3,3));o=0,s=r,0===i?(c=138,f=3):r==i?(c=6,f=3):(c=7,f=4)}}function $(){16==O?(Q(F),F=0,O=0):8>O||(J(255&F),F>>>=8,O-=8)}function ee(t,n){let s,i,o;if(e.Ne[H]=t,e.Te[H]=255&n,H++,0===t?M[2*n]++:(L++,t--,M[2*(Ie.ge[n]+256+1)]++,K[2*Ie.Se(t)]++),0==(8191&H)&&V>2){for(s=8*H,i=C-k,o=0;30>o;o++)s+=K[2*o]*(5+Ie.Ce[o]);if(s>>>=3,Lc);Y(256,t),j=t[513]}function ne(){O>8?Q(F):O>0&&J(255&F),F=0,O=0}function re(t,n,r){X(0+(r?1:0),3),((t,n)=>{ne(),j=8,Q(n),Q(~n),e.Ue.set(u.subarray(t,t+n),e.pending),e.pending+=n})(t,n)}function se(n){((t,n,r)=>{let s,i,o=0;V>0?(U.ne(e),N.ne(e),o=(()=>{let t;for(G(M,U.he),G(K,N.he),T.ne(e),t=18;t>=3&&0===P[2*Ie.xe[t]+1];t--);return e.ue+=14+3*(t+1),t})(),s=e.ue+3+7>>>3,i=e.we+3+7>>>3,i>s||(s=i)):s=i=n+5,n+4>s||-1==t?i==s?(X(2+(r?1:0),3),te(xe._e,xe.Ee)):(X(4+(r?1:0),3),((e,t,n)=>{let r;for(X(e-257,5),X(t-1,5),X(n-4,4),r=0;n>r;r++)X(P[2*Ie.xe[r]+1],3);Z(M,e-1),Z(K,t-1)})(U.he+1,N.he+1,o+1),te(M,K)):re(t,n,r),q(),r&&ne()})(0>k?-1:k,C-k,n),k=C,t.We()}function ie(){let e,n,r,s;do{if(s=w-x-C,0===s&&0===C&&0===x)s=f;else if(-1==s)s--;else if(C>=f+f-262){u.set(u.subarray(f,f+f),0),I-=f,C-=f,k-=f,e=y,r=e;do{n=65535&d[--r],d[r]=f>n?0:n-f}while(0!=--e);e=f,r=e;do{n=65535&h[--r],h[r]=f>n?0:n-f}while(0!=--e);s+=f}if(0===t.He)return;e=t.Le(u,C+x,s),x+=e,3>x||(p=255&u[C],p=(p<x&&0!==t.He)}function oe(e){let t,n,r=A,s=C,i=_;const o=C>f-262?C-(f-262):0;let c=E;const a=l,w=C+258;let d=u[s+i-1],p=u[s+i];B>_||(r>>=2),c>x&&(c=x);do{if(t=e,u[t+i]==p&&u[t+i-1]==d&&u[t]==u[s]&&u[++t]==u[s+1]){s+=2,t++;do{}while(u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&u[++s]==u[++t]&&w>s);if(n=258-(w-s),s=w-258,n>i){if(I=e,i=n,n>=c)break;d=u[s+i-1],p=u[s+i]}}}while((e=65535&h[e&a])>o&&0!=--r);return i>x?x:i}e.le=[],e.be=[],e.ae=[],M=[],K=[],P=[],e.de=(t,n)=>{const r=e.ae,s=r[n];let i=n<<1;for(;i<=e.ce&&(i(H||(H=8),L||(L=8),G||(G=0),t.Fe=null,-1==S&&(S=6),1>L||L>9||8!=H||9>I||I>15||0>S||S>9||0>G||G>2?-2:(t.Oe=e,a=I,f=1<(t.qe=t.Ge=0,t.Fe=null,e.pending=0,e.Je=0,n=113,c=0,U.re=M,U.ie=xe.Me,N.re=K,N.ie=xe.Ke,T.re=P,T.ie=xe.Pe,F=0,O=0,j=8,q(),(()=>{w=2*f,d[y-1]=0;for(let e=0;y-1>e;e++)d[e]=0;D=Ve[V].De,B=Ve[V].Ae,E=Ve[V].Ve,A=Ve[V].Re,C=0,k=0,x=0,v=_=2,z=0,p=0})(),0))(t))),e.Qe=()=>42!=n&&113!=n&&666!=n?-2:(e.Te=null,e.Ne=null,e.Ue=null,d=null,h=null,u=null,e.Oe=null,113==n?-3:0),e.Xe=(e,t,n)=>{let r=0;return-1==t&&(t=6),0>t||t>9||0>n||n>2?-2:(Ve[V].Be!=Ve[t].Be&&0!==e.qe&&(r=e.Ye(1)),V!=t&&(V=t,D=Ve[V].De,B=Ve[V].Ae,E=Ve[V].Ve,A=Ve[V].Re),R=n,r)},e.Ze=(e,t,r)=>{let s,i=r,o=0;if(!t||42!=n)return-2;if(3>i)return 0;for(i>f-262&&(i=f-262,o=r-i),u.set(t.subarray(o,o+i),0),C=i,k=i,p=255&u[0],p=(p<=s;s++)p=(p<{let o,w,m,A,B;if(i>4||0>i)return-2;if(!r.$e||!r.et&&0!==r.He||666==n&&4!=i)return r.Fe=Re[4],-2;if(0===r.tt)return r.Fe=Re[7],-5;var E;if(t=r,A=c,c=i,42==n&&(w=8+(a-8<<4)<<8,m=(V-1&255)>>1,m>3&&(m=3),w|=m<<6,0!==C&&(w|=32),w+=31-w%31,n=113,J((E=w)>>8&255),J(255&E)),0!==e.pending){if(t.We(),0===t.tt)return c=-1,0}else if(0===t.He&&A>=i&&4!=i)return t.Fe=Re[7],-5;if(666==n&&0!==t.He)return r.Fe=Re[7],-5;if(0!==t.He||0!==x||0!=i&&666!=n){switch(B=-1,Ve[V].Be){case 0:B=(e=>{let n,r=65535;for(r>s-5&&(r=s-5);;){if(1>=x){if(ie(),0===x&&0==e)return 0;if(0===x)break}if(C+=x,x=0,n=k+r,(0===C||C>=n)&&(x=C-n,C=n,se(!1),0===t.tt))return 0;if(C-k>=f-262&&(se(!1),0===t.tt))return 0}return se(4==e),0===t.tt?4==e?2:0:4==e?3:1})(i);break;case 1:B=(e=>{let n,r=0;for(;;){if(262>x){if(ie(),262>x&&0==e)return 0;if(0===x)break}if(3>x||(p=(p<f-262||2!=R&&(v=oe(r)),3>v)n=ee(0,255&u[C]),x--,C++;else if(n=ee(C-I,v-3),x-=v,v>D||3>x)C+=v,v=0,p=255&u[C],p=(p<{let n,r,s=0;for(;;){if(262>x){if(ie(),262>x&&0==e)return 0;if(0===x)break}if(3>x||(p=(p<_&&f-262>=(C-s&65535)&&(2!=R&&(v=oe(s)),5>=v&&(1==R||3==v&&C-I>4096)&&(v=2)),3>_||v>_)if(0!==z){if(n=ee(0,255&u[C-1]),n&&se(!1),C++,x--,0===t.tt)return 0}else z=1,C++,x--;else{r=C+x-3,n=ee(C-1-S,_-3),x-=_-1,_-=2;do{++C>r||(p=(p<1+j+10-O&&(X(2,3),Y(256,xe._e),$()),j=7;else if(re(0,0,!1),3==i)for(o=0;y>o;o++)d[o]=0;if(t.We(),0===t.tt)return c=-1,0}}return 4!=i?0:1}}function Me(){const e=this;e.nt=0,e.rt=0,e.He=0,e.qe=0,e.tt=0,e.Ge=0}function Ke(e){const t=new Me,n=(o=e&&e.chunkSize?e.chunkSize:65536)+5*(r.floor(o/16383)+1);var o;const c=new i(n);let f=e?e.level:-1;void 0===f&&(f=-1),t.je(f),t.$e=c,this.append=(e,r)=>{let o,f,a=0,l=0,u=0;const w=[];if(e.length){t.nt=0,t.et=e,t.He=e.length;do{if(t.rt=0,t.tt=n,o=t.Ye(0),0!=o)throw new s("deflating: "+t.Fe);t.rt&&(t.rt==n?w.push(new i(c)):w.push(c.slice(0,t.rt))),u+=t.rt,r&&t.nt>0&&t.nt!=a&&(r(t.nt),a=t.nt)}while(t.He>0||0===t.tt);return w.length>1?(f=new i(u),w.forEach((e=>{f.set(e,l),l+=e.length}))):f=w[0]||new i,f}},this.flush=()=>{let e,r,o=0,f=0;const a=[];do{if(t.rt=0,t.tt=n,e=t.Ye(4),1!=e&&0!=e)throw new s("deflating: "+t.Fe);n-t.tt>0&&a.push(c.slice(0,t.rt)),f+=t.rt}while(t.He>0||0===t.tt);return t.Qe(),r=new i(f),a.forEach((e=>{r.set(e,o),o+=e.length})),r}}Me.prototype={je(e,t){const n=this;return n.Oe=new Ee,t||(t=15),n.Oe.je(n,e,t)},Ye(e){const t=this;return t.Oe?t.Oe.Ye(t,e):-2},Qe(){const e=this;if(!e.Oe)return-2;const t=e.Oe.Qe();return e.Oe=null,t},Xe(e,t){const n=this;return n.Oe?n.Oe.Xe(n,e,t):-2},Ze(e,t){const n=this;return n.Oe?n.Oe.Ze(n,e,t):-2},Le(e,t,n){const r=this;let s=r.He;return s>n&&(s=n),0===s?0:(r.He-=s,e.set(r.et.subarray(r.nt,r.nt+s),t),r.nt+=s,r.qe+=s,s)},We(){const e=this;let t=e.Oe.pending;t>e.tt&&(t=e.tt),0!==t&&(e.$e.set(e.Oe.Ue.subarray(e.Oe.Je,e.Oe.Je+t),e.rt),e.rt+=t,e.Oe.Je+=t,e.Ge+=t,e.tt-=t,e.Oe.pending-=t,0===e.Oe.pending&&(e.Oe.Je=0))}};const Pe=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],Ue=[96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,192,80,7,10,0,8,96,0,8,32,0,9,160,0,8,0,0,8,128,0,8,64,0,9,224,80,7,6,0,8,88,0,8,24,0,9,144,83,7,59,0,8,120,0,8,56,0,9,208,81,7,17,0,8,104,0,8,40,0,9,176,0,8,8,0,8,136,0,8,72,0,9,240,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,200,81,7,13,0,8,100,0,8,36,0,9,168,0,8,4,0,8,132,0,8,68,0,9,232,80,7,8,0,8,92,0,8,28,0,9,152,84,7,83,0,8,124,0,8,60,0,9,216,82,7,23,0,8,108,0,8,44,0,9,184,0,8,12,0,8,140,0,8,76,0,9,248,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,196,81,7,11,0,8,98,0,8,34,0,9,164,0,8,2,0,8,130,0,8,66,0,9,228,80,7,7,0,8,90,0,8,26,0,9,148,84,7,67,0,8,122,0,8,58,0,9,212,82,7,19,0,8,106,0,8,42,0,9,180,0,8,10,0,8,138,0,8,74,0,9,244,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,204,81,7,15,0,8,102,0,8,38,0,9,172,0,8,6,0,8,134,0,8,70,0,9,236,80,7,9,0,8,94,0,8,30,0,9,156,84,7,99,0,8,126,0,8,62,0,9,220,82,7,27,0,8,110,0,8,46,0,9,188,0,8,14,0,8,142,0,8,78,0,9,252,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,194,80,7,10,0,8,97,0,8,33,0,9,162,0,8,1,0,8,129,0,8,65,0,9,226,80,7,6,0,8,89,0,8,25,0,9,146,83,7,59,0,8,121,0,8,57,0,9,210,81,7,17,0,8,105,0,8,41,0,9,178,0,8,9,0,8,137,0,8,73,0,9,242,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,202,81,7,13,0,8,101,0,8,37,0,9,170,0,8,5,0,8,133,0,8,69,0,9,234,80,7,8,0,8,93,0,8,29,0,9,154,84,7,83,0,8,125,0,8,61,0,9,218,82,7,23,0,8,109,0,8,45,0,9,186,0,8,13,0,8,141,0,8,77,0,9,250,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,198,81,7,11,0,8,99,0,8,35,0,9,166,0,8,3,0,8,131,0,8,67,0,9,230,80,7,7,0,8,91,0,8,27,0,9,150,84,7,67,0,8,123,0,8,59,0,9,214,82,7,19,0,8,107,0,8,43,0,9,182,0,8,11,0,8,139,0,8,75,0,9,246,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,206,81,7,15,0,8,103,0,8,39,0,9,174,0,8,7,0,8,135,0,8,71,0,9,238,80,7,9,0,8,95,0,8,31,0,9,158,84,7,99,0,8,127,0,8,63,0,9,222,82,7,27,0,8,111,0,8,47,0,9,190,0,8,15,0,8,143,0,8,79,0,9,254,96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,193,80,7,10,0,8,96,0,8,32,0,9,161,0,8,0,0,8,128,0,8,64,0,9,225,80,7,6,0,8,88,0,8,24,0,9,145,83,7,59,0,8,120,0,8,56,0,9,209,81,7,17,0,8,104,0,8,40,0,9,177,0,8,8,0,8,136,0,8,72,0,9,241,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,201,81,7,13,0,8,100,0,8,36,0,9,169,0,8,4,0,8,132,0,8,68,0,9,233,80,7,8,0,8,92,0,8,28,0,9,153,84,7,83,0,8,124,0,8,60,0,9,217,82,7,23,0,8,108,0,8,44,0,9,185,0,8,12,0,8,140,0,8,76,0,9,249,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,197,81,7,11,0,8,98,0,8,34,0,9,165,0,8,2,0,8,130,0,8,66,0,9,229,80,7,7,0,8,90,0,8,26,0,9,149,84,7,67,0,8,122,0,8,58,0,9,213,82,7,19,0,8,106,0,8,42,0,9,181,0,8,10,0,8,138,0,8,74,0,9,245,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,205,81,7,15,0,8,102,0,8,38,0,9,173,0,8,6,0,8,134,0,8,70,0,9,237,80,7,9,0,8,94,0,8,30,0,9,157,84,7,99,0,8,126,0,8,62,0,9,221,82,7,27,0,8,110,0,8,46,0,9,189,0,8,14,0,8,142,0,8,78,0,9,253,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,195,80,7,10,0,8,97,0,8,33,0,9,163,0,8,1,0,8,129,0,8,65,0,9,227,80,7,6,0,8,89,0,8,25,0,9,147,83,7,59,0,8,121,0,8,57,0,9,211,81,7,17,0,8,105,0,8,41,0,9,179,0,8,9,0,8,137,0,8,73,0,9,243,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,203,81,7,13,0,8,101,0,8,37,0,9,171,0,8,5,0,8,133,0,8,69,0,9,235,80,7,8,0,8,93,0,8,29,0,9,155,84,7,83,0,8,125,0,8,61,0,9,219,82,7,23,0,8,109,0,8,45,0,9,187,0,8,13,0,8,141,0,8,77,0,9,251,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,199,81,7,11,0,8,99,0,8,35,0,9,167,0,8,3,0,8,131,0,8,67,0,9,231,80,7,7,0,8,91,0,8,27,0,9,151,84,7,67,0,8,123,0,8,59,0,9,215,82,7,19,0,8,107,0,8,43,0,9,183,0,8,11,0,8,139,0,8,75,0,9,247,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,207,81,7,15,0,8,103,0,8,39,0,9,175,0,8,7,0,8,135,0,8,71,0,9,239,80,7,9,0,8,95,0,8,31,0,9,159,84,7,99,0,8,127,0,8,63,0,9,223,82,7,27,0,8,111,0,8,47,0,9,191,0,8,15,0,8,143,0,8,79,0,9,255],Ne=[80,5,1,87,5,257,83,5,17,91,5,4097,81,5,5,89,5,1025,85,5,65,93,5,16385,80,5,3,88,5,513,84,5,33,92,5,8193,82,5,9,90,5,2049,86,5,129,192,5,24577,80,5,2,87,5,385,83,5,25,91,5,6145,81,5,7,89,5,1537,85,5,97,93,5,24577,80,5,4,88,5,769,84,5,49,92,5,12289,82,5,13,90,5,3073,86,5,193,192,5,24577],Te=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],We=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,112,112],He=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],Le=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];function je(){let e,t,n,r,s,i;function o(e,t,o,c,f,a,l,u,w,h,d){let p,y,m,b,g,k,v,S,z,C,I,x,_,A,D;C=0,g=o;do{n[e[t+C]]++,C++,g--}while(0!==g);if(n[0]==o)return l[0]=-1,u[0]=0,0;for(S=u[0],k=1;15>=k&&0===n[k];k++);for(v=k,k>S&&(S=k),g=15;0!==g&&0===n[g];g--);for(m=g,S>g&&(S=g),u[0]=S,A=1<k;k++,A<<=1)if(0>(A-=n[k]))return-3;if(0>(A-=n[g]))return-3;for(n[g]+=A,i[1]=k=0,C=1,_=2;0!=--g;)i[_]=k+=n[C],_++,C++;g=0,C=0;do{0!==(k=e[t+C])&&(d[i[k]++]=g),C++}while(++g=v;v++)for(p=n[v];0!=p--;){for(;v>x+S;){if(b++,x+=S,D=m-x,D=D>S?S:D,(y=1<<(k=v-x))>p+1&&(y-=p+1,_=v,D>k))for(;++kn[++_];)y-=n[_];if(D=1<1440)return-3;s[b]=I=h[0],h[0]+=D,0!==b?(i[b]=g,r[0]=k,r[1]=S,k=g>>>x-S,r[2]=I-s[b-1]-k,w.set(r,3*(s[b-1]+k))):l[0]=I}for(r[1]=v-x,o>C?d[C]d[C]?0:96,r[2]=d[C++]):(r[0]=a[d[C]-c]+16+64,r[2]=f[d[C++]-c]):r[0]=192,y=1<>>x;D>k;k+=y)w.set(r,3*(I+k));for(k=1<>>=1)g^=k;for(g^=k,z=(1<c;c++)t[c]=0;for(c=0;16>c;c++)n[c]=0;for(c=0;3>c;c++)r[c]=0;s.set(n.subarray(0,15),0),i.set(n.subarray(0,16),0)}this.st=(n,r,s,i,f)=>{let a;return c(19),e[0]=0,a=o(n,0,19,19,null,null,s,r,i,e,t),-3==a?f.Fe="oversubscribed dynamic bit lengths tree":-5!=a&&0!==r[0]||(f.Fe="incomplete dynamic bit lengths tree",a=-3),a},this.it=(n,r,s,i,f,a,l,u,w)=>{let h;return c(288),e[0]=0,h=o(s,0,n,257,Te,We,a,i,u,e,t),0!=h||0===i[0]?(-3==h?w.Fe="oversubscribed literal/length tree":-4!=h&&(w.Fe="incomplete literal/length tree",h=-3),h):(c(288),h=o(s,n,r,0,He,Le,l,f,u,e,t),0!=h||0===f[0]&&n>257?(-3==h?w.Fe="oversubscribed distance tree":-5==h?(w.Fe="incomplete distance tree",h=-3):-4!=h&&(w.Fe="empty distance tree with lengths",h=-3),h):0)}}function Fe(){const e=this;let t,n,r,s,i=0,o=0,c=0,f=0,a=0,l=0,u=0,w=0,h=0,d=0;function p(e,t,n,r,s,i,o,c){let f,a,l,u,w,h,d,p,y,m,b,g,k,v,S,z;d=c.nt,p=c.He,w=o.ot,h=o.ct,y=o.write,m=yh;)p--,w|=(255&c.ft(d++))<>=a[z+1],h-=a[z+1],0!=(16&u)){for(u&=15,k=a[z+2]+(w&Pe[u]),w>>=u,h-=u;15>h;)p--,w|=(255&c.ft(d++))<>=a[z+1],h-=a[z+1],0!=(16&u)){for(u&=15;u>h;)p--,w|=(255&c.ft(d++))<>=u,h-=u,m-=k,v>y){S=y-v;do{S+=o.end}while(0>S);if(u=o.end-S,k>u){if(k-=u,y-S>0&&u>y-S)do{o.lt[y++]=o.lt[S++]}while(0!=--u);else o.lt.set(o.lt.subarray(S,S+u),y),y+=u,S+=u,u=0;S=0}}else S=y-v,y-S>0&&2>y-S?(o.lt[y++]=o.lt[S++],o.lt[y++]=o.lt[S++],k-=2):(o.lt.set(o.lt.subarray(S,S+2),y),y+=2,S+=2,k-=2);if(y-S>0&&k>y-S)do{o.lt[y++]=o.lt[S++]}while(0!=--k);else o.lt.set(o.lt.subarray(S,S+k),y),y+=k,S+=k,k=0;break}if(0!=(64&u))return c.Fe="invalid distance code",k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,-3;f+=a[z+2],f+=w&Pe[u],z=3*(l+f),u=a[z]}break}if(0!=(64&u))return 0!=(32&u)?(k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,1):(c.Fe="invalid literal/length code",k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,-3);if(f+=a[z+2],f+=w&Pe[u],z=3*(l+f),0===(u=a[z])){w>>=a[z+1],h-=a[z+1],o.lt[y++]=a[z+2],m--;break}}else w>>=a[z+1],h-=a[z+1],o.lt[y++]=a[z+2],m--}while(m>=258&&p>=10);return k=c.He-p,k=k>h>>3?h>>3:k,p+=k,d-=k,h-=k<<3,o.ot=w,o.ct=h,c.He=p,c.qe+=d-c.nt,c.nt=d,o.write=y,0}e.init=(e,i,o,c,f,a)=>{t=0,u=e,w=i,r=o,h=c,s=f,d=a,n=null},e.ut=(e,y,m)=>{let b,g,k,v,S,z,C,I=0,x=0,_=0;for(_=y.nt,v=y.He,I=e.ot,x=e.ct,S=e.write,z=S=258&&v>=10&&(e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,m=p(u,w,r,h,s,d,e,y),_=y.nt,v=y.He,I=e.ot,x=e.ct,S=e.write,z=Sx;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>>=n[g+1],x-=n[g+1],k=n[g],0===k){f=n[g+2],t=6;break}if(0!=(16&k)){a=15&k,i=n[g+2],t=2;break}if(0==(64&k)){c=k,o=g/3+n[g+2];break}if(0!=(32&k)){t=7;break}return t=9,y.Fe="invalid literal/length code",m=-3,e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);case 2:for(b=a;b>x;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>=b,x-=b,c=w,n=s,o=d,t=3;case 3:for(b=c;b>x;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>=n[g+1],x-=n[g+1],k=n[g],0!=(16&k)){a=15&k,l=n[g+2],t=4;break}if(0==(64&k)){c=k,o=g/3+n[g+2];break}return t=9,y.Fe="invalid distance code",m=-3,e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);case 4:for(b=a;b>x;){if(0===v)return e.ot=I,e.ct=x,y.He=v,y.qe+=_-y.nt,y.nt=_,e.write=S,e.wt(y,m);m=0,v--,I|=(255&y.ft(_++))<>=b,x-=b,t=5;case 5:for(C=S-l;0>C;)C+=e.end;for(;0!==i;){if(0===z&&(S==e.end&&0!==e.read&&(S=0,z=S7&&(x-=8,v++,_--),e.write=S,m=e.wt(y,m),S=e.write,z=S{}}je.dt=(e,t,n,r)=>(e[0]=9,t[0]=5,n[0]=Ue,r[0]=Ne,0);const Oe=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];function qe(e,t){const n=this;let r,s=0,o=0,c=0,a=0;const l=[0],u=[0],w=new Fe;let h=0,d=new f(4320);const p=new je;n.ct=0,n.ot=0,n.lt=new i(t),n.end=t,n.read=0,n.write=0,n.reset=(e,t)=>{t&&(t[0]=0),6==s&&w.ht(e),s=0,n.ct=0,n.ot=0,n.read=n.write=0},n.reset(e,null),n.wt=(e,t)=>{let r,s,i;return s=e.rt,i=n.read,r=(i>n.write?n.end:n.write)-i,r>e.tt&&(r=e.tt),0!==r&&-5==t&&(t=0),e.tt-=r,e.Ge+=r,e.$e.set(n.lt.subarray(i,i+r),s),s+=r,i+=r,i==n.end&&(i=0,n.write==n.end&&(n.write=0),r=n.write-i,r>e.tt&&(r=e.tt),0!==r&&-5==t&&(t=0),e.tt-=r,e.Ge+=r,e.$e.set(n.lt.subarray(i,i+r),s),s+=r,i+=r),e.rt=s,n.read=i,t},n.ut=(e,t)=>{let i,f,y,m,b,g,k,v;for(m=e.nt,b=e.He,f=n.ot,y=n.ct,g=n.write,k=gy;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>1){case 0:f>>>=3,y-=3,i=7&y,f>>>=i,y-=i,s=1;break;case 1:S=[],z=[],C=[[]],I=[[]],je.dt(S,z,C,I),w.init(S[0],z[0],C[0],0,I[0],0),f>>>=3,y-=3,s=6;break;case 2:f>>>=3,y-=3,s=3;break;case 3:return f>>>=3,y-=3,s=9,e.Fe="invalid block type",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t)}break;case 1:for(;32>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>16&65535)!=(65535&f))return s=9,e.Fe="invalid stored block lengths",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);o=65535&f,f=y=0,s=0!==o?2:0!==h?7:0;break;case 2:if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);if(0===k&&(g==n.end&&0!==n.read&&(g=0,k=gb&&(i=b),i>k&&(i=k),n.lt.set(e.Le(m,i),g),m+=i,b-=i,g+=i,k-=i,0!=(o-=i))break;s=0!==h?7:0;break;case 3:for(;14>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<29||(i>>5&31)>29)return s=9,e.Fe="too many length or distance symbols",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);if(i=258+(31&i)+(i>>5&31),!r||r.lengthv;v++)r[v]=0;f>>>=14,y-=14,a=0,s=4;case 4:for(;4+(c>>>10)>a;){for(;3>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>=3,y-=3}for(;19>a;)r[Oe[a++]]=0;if(l[0]=7,i=p.st(r,l,u,d,e),0!=i)return-3==(t=i)&&(r=null,s=9),n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);a=0,s=5;case 5:for(;i=c,258+(31&i)+(i>>5&31)>a;){let o,w;for(i=l[0];i>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<w)f>>>=i,y-=i,r[a++]=w;else{for(v=18==w?7:w-14,o=18==w?11:3;i+v>y;){if(0===b)return n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);t=0,b--,f|=(255&e.ft(m++))<>>=i,y-=i,o+=f&Pe[v],f>>>=v,y-=v,v=a,i=c,v+o>258+(31&i)+(i>>5&31)||16==w&&1>v)return r=null,s=9,e.Fe="invalid bit length repeat",t=-3,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);w=16==w?r[v-1]:0;do{r[v++]=w}while(0!=--o);a=v}}if(u[0]=-1,x=[],_=[],A=[],D=[],x[0]=9,_[0]=6,i=c,i=p.it(257+(31&i),1+(i>>5&31),r,x,_,A,D,d,e),0!=i)return-3==i&&(r=null,s=9),t=i,n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,n.wt(e,t);w.init(x[0],_[0],d,A[0],d,D[0]),s=6;case 6:if(n.ot=f,n.ct=y,e.He=b,e.qe+=m-e.nt,e.nt=m,n.write=g,1!=(t=w.ut(n,e,t)))return n.wt(e,t);if(t=0,w.ht(e),m=e.nt,b=e.He,f=n.ot,y=n.ct,g=n.write,k=g{n.reset(e,null),n.lt=null,d=null},n.yt=(e,t,r)=>{n.lt.set(e.subarray(t,t+r),0),n.read=n.write=r},n.bt=()=>1==s?1:0}const Ge=[0,0,255,255];function Je(){const e=this;function t(e){return e&&e.gt?(e.qe=e.Ge=0,e.Fe=null,e.gt.mode=7,e.gt.kt.reset(e,null),0):-2}e.mode=0,e.method=0,e.vt=[0],e.St=0,e.marker=0,e.zt=0,e.Ct=t=>(e.kt&&e.kt.ht(t),e.kt=null,0),e.It=(n,r)=>(n.Fe=null,e.kt=null,8>r||r>15?(e.Ct(n),-2):(e.zt=r,n.gt.kt=new qe(n,1<{let n,r;if(!e||!e.gt||!e.et)return-2;const s=e.gt;for(t=4==t?-5:0,n=-5;;)switch(s.mode){case 0:if(0===e.He)return n;if(n=t,e.He--,e.qe++,8!=(15&(s.method=e.ft(e.nt++)))){s.mode=13,e.Fe="unknown compression method",s.marker=5;break}if(8+(s.method>>4)>s.zt){s.mode=13,e.Fe="invalid win size",s.marker=5;break}s.mode=1;case 1:if(0===e.He)return n;if(n=t,e.He--,e.qe++,r=255&e.ft(e.nt++),((s.method<<8)+r)%31!=0){s.mode=13,e.Fe="incorrect header check",s.marker=5;break}if(0==(32&r)){s.mode=7;break}s.mode=2;case 2:if(0===e.He)return n;n=t,e.He--,e.qe++,s.St=(255&e.ft(e.nt++))<<24&4278190080,s.mode=3;case 3:if(0===e.He)return n;n=t,e.He--,e.qe++,s.St+=(255&e.ft(e.nt++))<<16&16711680,s.mode=4;case 4:if(0===e.He)return n;n=t,e.He--,e.qe++,s.St+=(255&e.ft(e.nt++))<<8&65280,s.mode=5;case 5:return 0===e.He?n:(n=t,e.He--,e.qe++,s.St+=255&e.ft(e.nt++),s.mode=6,2);case 6:return s.mode=13,e.Fe="need dictionary",s.marker=0,-2;case 7:if(n=s.kt.ut(e,n),-3==n){s.mode=13,s.marker=0;break}if(0==n&&(n=t),1!=n)return n;n=t,s.kt.reset(e,s.vt),s.mode=12;case 12:return e.He=0,1;case 13:return-3;default:return-2}},e._t=(e,t,n)=>{let r=0,s=n;if(!e||!e.gt||6!=e.gt.mode)return-2;const i=e.gt;return s<1<{let n,r,s,i,o;if(!e||!e.gt)return-2;const c=e.gt;if(13!=c.mode&&(c.mode=13,c.marker=0),0===(n=e.He))return-5;for(r=e.nt,s=c.marker;0!==n&&4>s;)e.ft(r)==Ge[s]?s++:s=0!==e.ft(r)?0:4-s,r++,n--;return e.qe+=r-e.nt,e.nt=r,e.He=n,c.marker=s,4!=s?-3:(i=e.qe,o=e.Ge,t(e),e.qe=i,e.Ge=o,c.mode=7,0)},e.Dt=e=>e&&e.gt&&e.gt.kt?e.gt.kt.bt():-2}function Qe(){}function Xe(e){const t=new Qe,n=e&&e.chunkSize?r.floor(2*e.chunkSize):131072,o=new i(n);let c=!1;t.It(),t.$e=o,this.append=(e,r)=>{const f=[];let a,l,u=0,w=0,h=0;if(0!==e.length){t.nt=0,t.et=e,t.He=e.length;do{if(t.rt=0,t.tt=n,0!==t.He||c||(t.nt=0,c=!0),a=t.xt(0),c&&-5===a){if(0!==t.He)throw new s("inflating: bad input")}else if(0!==a&&1!==a)throw new s("inflating: "+t.Fe);if((c||1===a)&&t.He===e.length)throw new s("inflating: bad input");t.rt&&(t.rt===n?f.push(new i(o)):f.push(o.slice(0,t.rt))),h+=t.rt,r&&t.nt>0&&t.nt!=u&&(r(t.nt),u=t.nt)}while(t.He>0||0===t.tt);return f.length>1?(l=new i(h),f.forEach((e=>{l.set(e,w),w+=e.length}))):l=f[0]||new i,l}},this.flush=()=>{t.Ct()}}Qe.prototype={It(e){const t=this;return t.gt=new Je,e||(e=15),t.gt.It(t,e)},xt(e){const t=this;return t.gt?t.gt.xt(t,e):-2},Ct(){const e=this;if(!e.gt)return-2;const t=e.gt.Ct(e);return e.gt=null,t},At(){const e=this;return e.gt?e.gt.At(e):-2},_t(e,t){const n=this;return n.gt?n.gt._t(n,e,t):-2},ft(e){return this.et[e]},Le(e,t){return this.et.subarray(e,e+t)}},self.initCodec=()=>{self.Deflate=Ke,self.Inflate=Xe};\n'],{type:"text/javascript"}));e({workerScripts:{inflate:[t],deflate:[t]}})})(Q),e.BlobReader=It,e.BlobWriter=At,e.Data64URIReader=class extends Ft{constructor(e){super();let t=e.length;for(;"="==e.charAt(t-1);)t--;const r=e.indexOf(",")+1;n.assign(this,{dataURI:e,dataStart:r,size:a.floor(.75*(t-r))})}readUint8Array(e,t){const{dataStart:n,dataURI:r}=this,s=new w(t),i=4*a.floor(e/3),o=atob(r.substring(i+n,4*a.ceil((e+t)/3)+n)),c=e-3*a.floor(i/4);for(let e=c;c+t>e;e++)s[e-c]=o.charCodeAt(e);return s}},e.Data64URIWriter=class extends Et{constructor(e){super(),n.assign(this,{data:"data:"+(e||"")+";base64,",pending:[]})}writeUint8Array(e){const t=this;let n=0,s=t.pending;const i=t.pending.length;for(t.pending="",n=0;n<3*a.floor((i+e.length)/3)-i;n++)s+=r.fromCharCode(e[n]);for(;n2?t.data+=x(s):t.pending=s}getData(){return this.data+x(this.pending)}},e.ERR_BAD_FORMAT=zn,e.ERR_CENTRAL_DIRECTORY_NOT_FOUND=_n,e.ERR_DUPLICATED_NAME=Gn,e.ERR_ENCRYPTED=Fn,e.ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUND=Cn,e.ERR_EOCDR_NOT_FOUND=xn,e.ERR_EOCDR_ZIP64_NOT_FOUND=vn,e.ERR_EXTRAFIELD_ZIP64_NOT_FOUND=Rn,e.ERR_HTTP_RANGE=xt,e.ERR_INVALID_COMMENT=jn,e.ERR_INVALID_ENCRYPTION_STRENGTH=er,e.ERR_INVALID_ENTRY_COMMENT=Jn,e.ERR_INVALID_ENTRY_NAME=Qn,e.ERR_INVALID_EXTRAFIELD_DATA=nr,e.ERR_INVALID_EXTRAFIELD_TYPE=tr,e.ERR_INVALID_PASSWORD=ue,e.ERR_INVALID_SIGNATURE=fe,e.ERR_INVALID_VERSION=$n,e.ERR_ITERATOR_COMPLETED_TOO_SOON=vt,e.ERR_LOCAL_FILE_HEADER_NOT_FOUND=Dn,e.ERR_SPLIT_ZIP_FILE=An,e.ERR_UNSUPPORTED_COMPRESSION=In,e.ERR_UNSUPPORTED_ENCRYPTION=En,e.ERR_UNSUPPORTED_FORMAT=rr,e.HttpRangeReader=class extends Yt{constructor(e,t={}){t.useRangeHeader=!0,super(e,t)}},e.HttpReader=Yt,e.Reader=Ft,e.SplitDataReader=Zt,e.SplitDataWriter=Xt,e.SplitZipReader=$t,e.SplitZipWriter=en,e.TextReader=class extends It{constructor(e){super(new m([e],{type:"text/plain"}))}},e.TextWriter=class extends At{constructor(e){super(e),n.assign(this,{encoding:e,utf8:!e||"utf-8"==e.toLowerCase()})}async getData(){const{encoding:e,utf8:t}=this,r=await super.getData();if(r.text&&t)return r.text();{const t=new FileReader;return new y(((s,i)=>{n.assign(t,{onload:({target:e})=>s(e.result),onerror:()=>i(t.error)}),t.readAsText(r,e)}))}}},e.Uint8ArrayReader=class extends Ft{constructor(e){super(),n.assign(this,{array:e,size:e.length})}readUint8Array(e,t){return this.array.slice(e,e+t)}},e.Uint8ArrayWriter=class extends Et{init(e=0){super.init(),n.assign(this,{offset:0,array:new w(e)})}writeUint8Array(e){const t=this;if(t.offset+e.length>t.array.length){const n=t.array;t.array=new w(n.length+e.length),t.array.set(n)}t.array.set(e,t.offset),t.offset+=e.length}getData(){return this.array}},e.Writer=Et,e.ZipReader=class{constructor(e,t={}){n.assign(this,{reader:jt(e),options:t,config:j()})}async*getEntriesGenerator(e={}){const t=this;let{reader:r}=t;const{config:s}=t;if(await Gt(r),r.size!==V&&r.readUint8Array||(r=new It(await new u(r.readable).blob()),await Gt(r)),22>r.size)throw new d(zn);r.chunkSize=J(s);const i=await(async(e,t,n)=>{const r=new w(4);return Xn(r).setUint32(0,101010256,!0),await s(22)||await s(a.min(1048582,n));async function s(t){const s=n-t,i=await Qt(e,s,t);for(let e=i.length-22;e>=0;e--)if(i[e]==r[0]&&i[e+1]==r[1]&&i[e+2]==r[2]&&i[e+3]==r[3])return{offset:s+e,buffer:i.slice(e,e+22).buffer}}})(r,0,r.size);if(!i)throw Yn(Xn(await Qt(r,0,4)))==H?new d(An):new d(xn);const o=Xn(i);let c=Yn(o,12),l=Yn(o,16);const f=i.offset,h=Kn(o,20),p=f+22+h;let g=Kn(o,4);const m=r.lastDiskNumber||0;let b=Kn(o,6),S=Kn(o,8),k=0,z=0;if(l==I||c==I||S==A||b==A){const e=Xn(await Qt(r,i.offset-20,20));if(Yn(e,0)!=N)throw new d(vn);l=Zn(e,8);let t=await Qt(r,l,56,-1),n=Xn(t);const s=i.offset-20-56;if(Yn(n,0)!=U&&l!=s){const e=l;l=s,k=l-e,t=await Qt(r,l,56,-1),n=Xn(t)}if(Yn(n,0)!=U)throw new d(Cn);g==A&&(g=Yn(n,16)),b==A&&(b=Yn(n,20)),S==A&&(S=Zn(n,32)),c==I&&(c=Zn(n,40)),l-=c}if(m!=g)throw new d(An);if(0>l||l>=r.size)throw new d(zn);let x=0,v=await Qt(r,l,c,b),C=Xn(v);if(c){const e=i.offset-c;if(Yn(C,x)!=O&&l!=e){const t=l;l=e,k=l-t,v=await Qt(r,l,c,b),C=Xn(v)}}if(0>l||l>=r.size)throw new d(zn);const _=Mn(t,e,"filenameEncoding"),D=Mn(t,e,"commentEncoding");for(let i=0;S>i;i++){const o=new Nn(r,s,t.options);if(Yn(C,x)!=O)throw new d(_n);Wn(o,C,x+6);const c=!!o.bitFlag.languageEncodingFlag,l=x+46,u=l+o.filenameLength,f=u+o.extraFieldLength,w=Kn(C,x+4),h=0==(0&w),p=v.subarray(l,u),g=Kn(C,x+32),m=f+g,b=v.subarray(f,m),R=c,F=c,E=h&&16==(16&Bn(C,x+38)),I=Yn(C,x+42)+k;n.assign(o,{versionMadeBy:w,msDosCompatible:h,compressedSize:0,uncompressedSize:0,commentLength:g,directory:E,offset:I,diskNumberStart:Kn(C,x+34),internalFileAttribute:Kn(C,x+36),externalFileAttribute:Yn(C,x+38),rawFilename:p,filenameUTF8:R,commentUTF8:F,rawExtraField:v.subarray(u,f)});const[A,T]=await y.all([nn(p,R?Tn:_||Hn),nn(b,F?Tn:D||Hn)]);n.assign(o,{rawComment:b,filename:A,comment:T,directory:E||A.endsWith(L)}),z=a.max(I,z),await qn(o,o,C,x+6);const H=new kn(o);H.getData=(e,t)=>o.getData(e,H,t),x=m;const{onprogress:U}=e;if(U)try{await U(i+1,S,new kn(o))}catch(e){}yield H}const R=Mn(t,e,"extractPrependedData"),F=Mn(t,e,"extractAppendedData");return R&&(t.prependedData=z>0?await Qt(r,0,z):new w),t.comment=h?await Qt(r,f+22,h):new w,F&&(t.appendedData=par.push(e)));try{if(e=e.trim(),c.filenames.has(e))throw new d(Gn);return c.filenames.add(e),f=(async(e,r,s,c)=>{r=r.trim(),c.directory&&!r.endsWith(L)?r+=L:c.directory=r.endsWith(L);const l=se(r);if(gr(l)>A)throw new d(Qn);const u=c.comment||"",f=se(u);if(gr(f)>A)throw new d(Jn);const m=lr(e,c,"version",20);if(m>A)throw new d($n);const b=lr(e,c,"versionMadeBy",20);if(b>A)throw new d($n);const S=lr(e,c,dn,new o),k=lr(e,c,hn),z=lr(e,c,pn),x=lr(e,c,yn,!0),v=lr(e,c,gn,0),C=lr(e,c,mn,0),_=lr(e,c,"password"),D=lr(e,c,"encryptionStrength",3),R=lr(e,c,"zipCrypto"),F=lr(e,c,"extendedTimestamp",!0),E=lr(e,c,"keepOrder",!0),O=lr(e,c,"level"),U=lr(e,c,"useWebWorkers"),N=lr(e,c,"bufferedWrite"),B=lr(e,c,"dataDescriptorSignature",!1),K=lr(e,c,"signal"),Y=lr(e,c,"useCompressionStream");let Z=lr(e,c,"dataDescriptor",!0),X=lr(e,c,bn);if(_!==V&&D!==V&&(1>D||D>3))throw new d(er);let G=new w;const{extraField:j}=c;if(j){let e=0,t=0;j.forEach((t=>e+=4+gr(t))),G=new w(e),j.forEach(((e,n)=>{if(n>A)throw new d(tr);if(gr(e)>A)throw new d(nr);hr(G,new h([n]),t),hr(G,new h([gr(e)]),t+2),hr(G,e,t+4),t+=4+gr(e)}))}let Q=0,$=0,ee=0;const te=!0===X;s&&(s=jt(s),await Gt(s),s.size===V?(Z=!0,(X||X===V)&&(X=!0,Q=I)):(ee=s.size,Q=(e=>e+5*(a.floor(e/16383)+1))(ee)));const{diskOffset:ne,diskNumber:re,maxSize:ie}=e.writer,ae=te||ee>=I,oe=te||Q>=I,ce=te||e.offset+e.pendingEntriesSize-ne>=I,le=lr(e,c,"supportZip64SplitFile",!0)&&te||re+a.ceil(e.pendingEntriesSize/ie)>=A;if(ce||ae||oe||le){if(!1===X||!E)throw new d(rr);X=!0}X=X||!1;const ue=(e=>{const{rawFilename:t,lastModDate:n,lastAccessDate:r,creationDate:s,password:i,level:o,zip64:c,zipCrypto:l,dataDescriptor:u,directory:f,rawExtraField:d,encryptionStrength:h,extendedTimestamp:g}=e,m=0!==o&&!f,y=!(!i||!gr(i));let b,S,k,z=e.version;if(y&&!l){b=new w(gr(sr)+2);const e=pr(b);fr(e,0,39169),hr(b,sr,2),ur(e,8,h)}else b=new w;if(g){k=new w(9+(r?4:0)+(s?4:0));const e=pr(k);fr(e,0,W),fr(e,2,gr(k)-4),ur(e,4,1+(r?2:0)+(s?4:0)),dr(e,5,a.floor(n.getTime()/1e3)),r&&dr(e,9,a.floor(r.getTime()/1e3)),s&&dr(e,13,a.floor(s.getTime()/1e3));try{S=new w(36);const e=pr(S),t=cr(n);fr(e,0,10),fr(e,2,32),fr(e,8,1),fr(e,10,24),wr(e,12,t),wr(e,20,cr(r)||t),wr(e,28,cr(s)||t)}catch(e){S=new w}}else S=k=new w;let x=q;u&&(x|=8);let v=0;m&&(v=8),c&&(z=z>45?z:45),y&&(x|=1,l||(z=z>51?z:51,v=99,m&&(b[9]=8)));const C=new w(26),_=pr(C);fr(_,0,z),fr(_,2,x),fr(_,4,v);const D=new p(1),R=pr(D);let F;F=P>n?P:n>M?M:n,fr(R,0,(F.getHours()<<6|F.getMinutes())<<5|F.getSeconds()/2),fr(R,2,(F.getFullYear()-1980<<4|F.getMonth()+1)<<5|F.getDate());const E=D[0];dr(_,6,E),fr(_,22,gr(t));const I=gr(b,k,S,d);fr(_,24,I);const A=new w(30+gr(t)+I);return dr(pr(A),0,T),hr(A,C,4),hr(A,t,30),hr(A,b,30+gr(t)),hr(A,k,30+gr(t,b)),hr(A,S,30+gr(t,b,k)),hr(A,d,30+gr(t,b,k,S)),{localHeaderArray:A,headerArray:C,headerView:_,lastModDate:n,rawLastModDate:E,encrypted:y,compressed:m,version:z,compressionMethod:v,rawExtraFieldExtendedTimestamp:k,rawExtraFieldNTFS:S,rawExtraFieldAES:b}})(c=n.assign({},c,{rawFilename:l,rawComment:f,version:m,versionMadeBy:b,lastModDate:S,lastAccessDate:k,creationDate:z,rawExtraField:G,zip64:X,zip64UncompressedSize:ae,zip64CompressedSize:oe,zip64Offset:ce,zip64DiskNumberStart:le,password:_,level:O,useWebWorkers:U,encryptionStrength:D,extendedTimestamp:F,zipCrypto:R,bufferedWrite:N,keepOrder:E,dataDescriptor:Z,dataDescriptorSignature:B,signal:K,msDosCompatible:x,internalFileAttribute:v,externalFileAttribute:C,useCompressionStream:Y})),fe=(e=>{const{zip64:t,dataDescriptor:n,dataDescriptorSignature:r}=e;let s,i=new w,a=0;return n&&(i=new w(t?r?24:20:r?16:12),s=pr(i),r&&(a=4,dr(s,0,134695760))),{dataDescriptorArray:i,dataDescriptorView:s,dataDescriptorOffset:a}})(c);let de;$=gr(ue.localHeaderArray,fe.dataDescriptorArray)+Q,e.pendingEntriesSize+=$;try{de=await(async(e,r,s,a,o)=>{const{files:c,writer:l}=e,{keepOrder:u,dataDescriptor:f,signal:h}=o,{headerInfo:p}=a,m=t.from(c.values()).pop();let b,S,k,z,x,v,C={};c.set(r,C);try{let t;u&&(t=m&&m.lock,C.lock=new y((e=>k=e))),o.bufferedWrite||e.writerLocked||e.bufferedWrites&&u||!f?(v=new At,v.writable.size=0,b=!0,e.bufferedWrites++,await Gt(l)):(v=l,await _()),await Gt(v);const{writable:p}=l;let{diskOffset:R}=l;if(e.addSplitZipSignature){delete e.addSplitZipSignature;const t=new w(4);dr(pr(t),0,H),await or(p,t),e.offset+=4}b||(await t,await D(p));const{diskNumber:F}=l;if(x=!0,C.diskNumberStart=F,C=await(async(e,t,{diskNumberStart:r,lock:s},a,o,c)=>{const{headerInfo:l,dataDescriptorInfo:u}=a,{localHeaderArray:f,headerArray:d,lastModDate:h,rawLastModDate:p,encrypted:g,compressed:m,version:y,compressionMethod:b,rawExtraFieldExtendedTimestamp:S,rawExtraFieldNTFS:k,rawExtraFieldAES:z}=l,{dataDescriptorArray:x}=u,{rawFilename:v,lastAccessDate:C,creationDate:_,password:D,level:R,zip64:F,zip64UncompressedSize:E,zip64CompressedSize:A,zip64Offset:T,zip64DiskNumberStart:H,zipCrypto:O,dataDescriptor:U,directory:N,versionMadeBy:W,rawComment:q,rawExtraField:L,useWebWorkers:M,onstart:P,onprogress:B,onend:K,signal:Y,encryptionStrength:Z,extendedTimestamp:X,msDosCompatible:G,internalFileAttribute:j,externalFileAttribute:Q,useCompressionStream:$}=c,ee={lock:s,versionMadeBy:W,zip64:F,directory:!!N,filenameUTF8:!0,rawFilename:v,commentUTF8:!0,rawComment:q,rawExtraFieldExtendedTimestamp:S,rawExtraFieldNTFS:k,rawExtraFieldAES:z,rawExtraField:L,extendedTimestamp:X,msDosCompatible:G,internalFileAttribute:j,externalFileAttribute:Q,diskNumberStart:r};let te,ne=0,re=0;const{writable:se}=t;if(e){e.chunkSize=J(o),await or(se,f);const t=e.readable,n=t.size=e.size,r={options:{codecType:st,level:R,password:D,encryptionStrength:Z,zipCrypto:g&&O,passwordVerification:g&&O&&p>>8&255,signed:!0,compressed:m,encrypted:g,useWebWorkers:M,useCompressionStream:$,transferStreams:!1},config:o,streamOptions:{signal:Y,size:n,onstart:P,onprogress:B,onend:K}},s=await St({readable:t,writable:se},r);se.size+=s.size,te=s.signature,re=e.size=t.size,ne=s.size}else await or(se,f);let ie;if(F){let e=4;E&&(e+=8),A&&(e+=8),T&&(e+=8),H&&(e+=4),ie=new w(e)}else ie=new w;return e&&((e,t)=>{const{signature:n,rawExtraFieldZip64:r,compressedSize:s,uncompressedSize:a,headerInfo:o,dataDescriptorInfo:c}=e,{headerView:l,encrypted:u}=o,{dataDescriptorView:f,dataDescriptorOffset:d}=c,{zip64:w,zip64UncompressedSize:h,zip64CompressedSize:p,zipCrypto:g,dataDescriptor:m}=t;if(u&&!g||n===V||(dr(l,10,n),m&&dr(f,d,n)),w){const e=pr(r);fr(e,0,1),fr(e,2,r.length-4);let t=4;h&&(dr(l,18,I),wr(e,t,i(a)),t+=8),p&&(dr(l,14,I),wr(e,t,i(s))),m&&(wr(f,d+4,i(s)),wr(f,d+12,i(a)))}else dr(l,14,s),dr(l,18,a),m&&(dr(f,d+4,s),dr(f,d+8,a))})({signature:te,rawExtraFieldZip64:ie,compressedSize:ne,uncompressedSize:re,headerInfo:l,dataDescriptorInfo:u},c),U&&await or(se,x),n.assign(ee,{uncompressedSize:re,compressedSize:ne,lastModDate:h,rawLastModDate:p,creationDate:_,lastAccessDate:C,encrypted:g,length:gr(f,x)+ne,compressionMethod:b,version:y,headerArray:d,signature:te,rawExtraFieldZip64:ie,zip64UncompressedSize:E,zip64CompressedSize:A,zip64Offset:T,zip64DiskNumberStart:H}),ee})(s,v,C,a,e.config,o),x=!1,c.set(r,C),C.filename=r,b){await v.writable.close();let e=await v.getData();await t,await _(),z=!0,f||(e=await(async(e,t,n,{zipCrypto:r})=>{const s=await(e=>e.slice(0,26).arrayBuffer())(t),i=new g(s);return e.encrypted&&!r||dr(i,14,e.signature),e.zip64?(dr(i,18,I),dr(i,22,I)):(dr(i,18,e.compressedSize),dr(i,22,e.uncompressedSize)),await or(n,new w(s)),t.slice(s.byteLength)})(C,e,p,o)),await D(p),C.diskNumberStart=l.diskNumber,R=l.diskOffset,await e.stream().pipeTo(p,{preventClose:!0,signal:h}),p.size+=e.size,z=!1}if(C.offset=e.offset-R,C.zip64)((e,t)=>{const{rawExtraFieldZip64:n,offset:r,diskNumberStart:s}=e,{zip64UncompressedSize:a,zip64CompressedSize:o,zip64Offset:c,zip64DiskNumberStart:l}=t,u=pr(n);let f=4;a&&(f+=8),o&&(f+=8),c&&(wr(u,f,i(r)),f+=8),l&&dr(u,f,s)})(C,o);else if(C.offset>=I)throw new d(rr);return e.offset+=C.length,C}catch(t){throw(b&&z||!b&&x)&&(e.hasCorruptedEntries=!0,t&&(t.corruptedEntry=!0),b?e.offset+=v.writable.size:e.offset=v.writable.size),c.delete(r),t}finally{b&&e.bufferedWrites--,k&&k(),S&&S()}async function _(){e.writerLocked=!0;const{lockWriter:t}=e;e.lockWriter=new y((t=>S=()=>{e.writerLocked=!1,t()})),await t}async function D(e){p.localHeaderArray.length>l.availableSize&&(l.availableSize=0,await or(e,new w))}})(e,r,s,{headerInfo:ue,dataDescriptorInfo:fe},c)}finally{e.pendingEntriesSize-=$}return n.assign(de,{name:r,comment:u,extraField:j}),new kn(de)})(c,e,r,s),l.add(f),await f}catch(t){throw c.filenames.delete(e),t}finally{l.delete(f);const e=ar.shift();e?e():ir--}}async close(e=new w,n={}){const{pendingAddFileCalls:r,writer:s}=this,{writable:o}=s;for(;r.size;)await y.all(t.from(r));return await(async(e,n,r)=>{const{files:s,writer:o}=e,{diskOffset:c,writable:l}=o;let{diskNumber:u}=o,f=0,h=0,p=e.offset-c,g=s.size;for(const[,{rawFilename:e,rawExtraFieldZip64:t,rawExtraFieldAES:n,rawExtraField:r,rawComment:i,rawExtraFieldExtendedTimestamp:a,rawExtraFieldNTFS:o}]of s)h+=46+gr(e,i,t,n,a,o,r);const m=new w(h),y=pr(m);await Gt(o);let b=0;for(const[e,n]of t.from(s.values()).entries()){const{offset:t,rawFilename:i,rawExtraFieldZip64:c,rawExtraFieldAES:u,rawExtraFieldNTFS:d,rawExtraField:h,rawComment:p,versionMadeBy:g,headerArray:S,directory:k,zip64:z,zip64UncompressedSize:x,zip64CompressedSize:v,zip64DiskNumberStart:C,zip64Offset:_,msDosCompatible:D,internalFileAttribute:R,externalFileAttribute:F,extendedTimestamp:E,lastModDate:T,diskNumberStart:H,uncompressedSize:U,compressedSize:N}=n;let q;if(E){q=new w(9);const e=pr(q);fr(e,0,W),fr(e,2,gr(q)-4),ur(e,4,1),dr(e,5,a.floor(T.getTime()/1e3))}else q=new w;const L=gr(c,u,q,d,h);dr(y,f,O),fr(y,f+4,g);const M=pr(S);x||dr(M,18,U),v||dr(M,14,N),hr(m,S,f+6),fr(y,f+30,L),fr(y,f+32,gr(p)),fr(y,f+34,z&&C?A:H),fr(y,f+36,R),F?dr(y,f+38,F):k&&D&&ur(y,f+38,16),dr(y,f+42,z&&_?I:t),hr(m,i,f+46),hr(m,c,f+46+gr(i)),hr(m,u,f+46+gr(i,c)),hr(m,q,f+46+gr(i,c,u)),hr(m,d,f+46+gr(i,c,u,q)),hr(m,h,f+46+gr(i,c,u,q,d)),hr(m,p,f+46+gr(i)+L);const P=46+gr(i,p)+L;if(f-b>o.availableSize&&(o.availableSize=0,await or(l,m.slice(b,f)),b=f),f+=P,r.onprogress)try{await r.onprogress(e+1,s.size,new kn(n))}catch(e){}}await or(l,b?m.slice(b):m);let S=o.diskNumber;const{availableSize:k}=o;22>k&&S++;let z=lr(e,r,"zip64");if(!(I>p&&I>h&&A>g&&A>S)){if(!1===z)throw new d(rr);z=!0}const x=new w(z?98:22),v=pr(x);f=0,z&&(dr(v,0,U),wr(v,4,i(44)),fr(v,12,45),fr(v,14,45),dr(v,16,S),dr(v,20,u),wr(v,24,i(g)),wr(v,32,i(g)),wr(v,40,i(h)),wr(v,48,i(p)),dr(v,56,N),wr(v,64,i(p)+i(h)),dr(v,72,S+1),lr(e,r,"supportZip64SplitFile",!0)&&(S=A,u=A),g=A,p=I,h=I,f+=76),dr(v,f,101010256),fr(v,f+4,S),fr(v,f+6,u),fr(v,f+8,g),fr(v,f+10,g),dr(v,f+12,h),dr(v,f+16,p);const C=gr(n);if(C){if(C>A)throw new d(jn);fr(v,f+20,C)}await or(l,x),C&&await or(l,n)})(this,e,n),lr(this,n,"preventClose")||await o.close(),s.getData?s.getData():o}},e.configure=Q,e.getMimeType=()=>"application/octet-stream",e.initReader=jt,e.initShimAsyncCodec=(e,t={},n)=>({Deflate:ee(e.Deflate,t.deflate,n),Inflate:ee(e.Inflate,t.inflate,n)}),e.initStream=Gt,e.initWriter=Jt,e.readUint8Array=Qt,e.terminateWorkers=()=>{mt.forEach((e=>{kt(e),e.terminate()}))},n.defineProperty(e,"__esModule",{value:!0})})); diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js new file mode 100644 index 0000000..e592ee5 --- /dev/null +++ b/public_included_ws_fallback/service-worker.js @@ -0,0 +1,148 @@ +const cacheVersion = 'v1.7.3'; +const cacheTitle = `moreload-included-ws-fallback-cache-${cacheVersion}`; +const urlsToCache = [ + 'index.html', + './', + 'styles.css', + 'scripts/network.js', + 'scripts/ui.js', + 'scripts/util.js', + 'scripts/qrcode.js', + 'scripts/zip.min.js', + 'scripts/NoSleep.min.js', + 'scripts/theme.js', + 'sounds/blop.mp3', + 'images/favicon-96x96.png', + 'images/favicon-96x96-notification.png', + 'images/android-chrome-192x192.png', + 'images/android-chrome-192x192-maskable.png', + 'images/android-chrome-512x512.png', + 'images/android-chrome-512x512-maskable.png', + 'images/apple-touch-icon.png', +]; + +self.addEventListener('install', function(event) { + // Perform install steps + event.waitUntil( + caches.open(cacheTitle) + .then(function(cache) { + return cache.addAll(urlsToCache).then(_ => { + console.log('All files cached.'); + }); + }) + ); +}); + +// fetch the resource from the network +const fromNetwork = (request, timeout) => + new Promise((fulfill, reject) => { + const timeoutId = setTimeout(reject, timeout); + fetch(request).then(response => { + clearTimeout(timeoutId); + fulfill(response); + update(request); + }, reject); + }); + +// fetch the resource from the browser cache +const fromCache = request => + caches + .open(cacheTitle) + .then(cache => + cache + .match(request) + .then(matching => matching || cache.match('/offline/')) + ); + +// cache the current page to make it available for offline +const update = request => + caches + .open(cacheTitle) + .then(cache => + fetch(request).then(response => { + cache.put(request, response).then(_ => { + console.log("Page successfully cached.") + }) + }) + ); + +// general strategy when making a request (eg if online try to fetch it +// from the network with a timeout, if something fails serve from cache) +self.addEventListener('fetch', function(event) { + if (event.request.method === "POST") { + // Requests related to Web Share Target. + event.respondWith((async () => { + let share_url = await evaluateRequestData(event.request); + share_url = event.request.url + share_url; + return Response.redirect(encodeURI(share_url), 302); + })()); + } else { + // Regular requests not related to Web Share Target. + event.respondWith( + fromNetwork(event.request, 10000).catch(() => fromCache(event.request)) + ); + event.waitUntil(update(event.request)); + } +}); + + +// on activation, we clean up the previously registered service workers +self.addEventListener('activate', evt => + evt.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== cacheTitle) { + return caches.delete(cacheName); + } + }) + ); + }) + ) +); + +const evaluateRequestData = async function (request) { + const formData = await request.formData(); + const title = formData.get("title"); + const text = formData.get("text"); + const url = formData.get("url"); + const files = formData.getAll("allfiles"); + + + return new Promise(async (resolve) => { + if (files && files.length > 0) { + let fileObjects = []; + for (let i=0; i { + const db = e.target.result; + for (let i = 0; i < fileObjects.length; i++) { + const transaction = db.transaction('share_target_files', 'readwrite'); + const objectStore = transaction.objectStore('share_target_files'); + + const objectStoreRequest = objectStore.add(fileObjects[i]); + objectStoreRequest.onsuccess = _ => { + if (i === fileObjects.length - 1) resolve('?share-target=files'); + } + } + } + DBOpenRequest.onerror = _ => { + resolve(''); + } + } else { + let share_url = '?share-target=text'; + + if (title) share_url += `&title=${title}`; + if (text) share_url += `&text=${text}`; + if (url) share_url += `&url=${url}`; + + resolve(share_url); + } + }); +} diff --git a/public_included_ws_fallback/sounds/blop.mp3 b/public_included_ws_fallback/sounds/blop.mp3 new file mode 100644 index 0000000..28a6244 Binary files /dev/null and b/public_included_ws_fallback/sounds/blop.mp3 differ diff --git a/public_included_ws_fallback/sounds/blop.ogg b/public_included_ws_fallback/sounds/blop.ogg new file mode 100644 index 0000000..d1ce0c2 Binary files /dev/null and b/public_included_ws_fallback/sounds/blop.ogg differ diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css new file mode 100644 index 0000000..fb2a5cb --- /dev/null +++ b/public_included_ws_fallback/styles.css @@ -0,0 +1,1500 @@ +/* Constants */ + +:root { + --icon-size: 24px; + --primary-color: #4285f4; + --paired-device-color: #00a69c; + --peer-width: 120px; + --ws-peer-color: #ff6b6b; + color-scheme: light dark; +} + +/* Layout */ + +html, +body { + margin: 0; + display: flex; + flex-direction: column; + width: 100vw; + overflow-x: hidden; + overscroll-behavior: none; + overflow-y: hidden; + /* Only allow selection on message and pair key */ + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +body { + height: 100%; + /* mobile viewport bug fix */ + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; +} + +html { + height: 100%; + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; +} + +.row-reverse { + display: flex; + flex-direction: row-reverse; +} + +.space-between { + justify-content: space-between; +} + +.row { + display: flex; + justify-content: center; + flex-direction: row; +} + +.column { + display: flex; + flex-direction: column; +} + +.center { + display: flex; + align-items: center; + justify-content: center; +} + +.grow { + flex-grow: 1; +} + +.full { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +header { + position: absolute; + align-items: baseline; + padding: 8px 16px; + box-sizing: border-box; + width: 100vw; + z-index: 2; + top: 0; + right: 0; +} + +header > a, +header > div { + margin-left: 8px; +} + +header > div { + display: flex; + flex-direction: column; + align-self: flex-start; + touch-action: manipulation; +} + +header > div .icon-button { + height: 40px; + transition: all 300ms; +} + +header > div > div { + display: flex; + flex-direction: column; +} + +header > div:not(:hover) .icon-button:not(.selected) { + height: 0; + opacity: 0; +} + +#theme-wrapper:hover::before { + border-radius: 20px; + background: currentColor; + opacity: 0.1; + transition: opacity 300ms; + content: ''; + position: absolute; + width: 40px; + top: 0; + bottom: 0; + margin-top: 8px; + margin-bottom: 8px; +} + +header > div:hover .icon-button.selected::before { + opacity: 0.1; +} + +@media (pointer: coarse) { + header > div:hover .icon-button.selected:hover::before { + opacity: 0.2; + } + + header > div .icon-button:not(.selected) { + height: 0; + opacity: 0; + pointer-events: none; + } + + header > div > div { + flex-direction: column-reverse; + } +} + +[hidden] { + display: none !important; +} + + +/* Typography */ + +body { + font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1 { + font-size: 34px; + font-weight: 400; + letter-spacing: -.01em; + line-height: 40px; + margin: 8px 0 0; +} + +h2 { + font-size: 24px; + font-weight: 400; + letter-spacing: -.012em; + line-height: 32px; + color: var(--primary-color);} + +h3 { + font-size: 20px; + font-weight: 500; + margin: 16px 0; + color: var(--primary-color); +} + +.font-subheading { + font-size: 14px; + font-weight: 400; + line-height: 18px; + word-break: normal; +} + +.text-center { + text-align: center; +} + +.font-body1, +body { + font-size: 14px; + font-weight: 400; + line-height: 20px; +} + +.font-body2 { + font-size: 12px; + line-height: 18px; +} + +a, +.icon-button { + text-decoration: none; + color: currentColor; + cursor: pointer; +} + +hr { + color: white; +} + +input { + cursor: pointer; +} + +input[type="checkbox"] { + min-width: 13px; +} + +x-noscript { + background: var(--primary-color); + color: white; + z-index: 2; +} + + +/* Icons */ + +.icon { + width: var(--icon-size); + height: var(--icon-size); + fill: currentColor; +} + + + +/* Shadows */ + +[shadow="1"] { + box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), + 0 1px 8px 0 rgba(0, 0, 0, 0.12), + 0 3px 3px -2px rgba(0, 0, 0, 0.4); +} + +[shadow="2"] { + box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), + 0 1px 10px 0 rgba(0, 0, 0, 0.12), + 0 2px 4px -1px rgba(0, 0, 0, 0.4); +} + + + + +/* Animations */ + +@keyframes fade-in { + 0% { + opacity: 0; + } +} + +#center { + position: relative; + display: flex; + margin-top: 56px; + flex-direction: column-reverse; + flex-grow: 1; + --footer-height: 146px; + max-height: calc(100vh - 56px - var(--footer-height)); + justify-content: space-around; + align-items: center; + overflow-x: hidden; + overflow-y: scroll; + overscroll-behavior-x: none; +} + +@media screen and (min-width: 402px) and (max-width: 425px) { + header:has(#edit-pair-devices:not([hidden]))~#center { + --footer-height: 164px; + } +} + +@media screen and (max-width: 402px) { + #center { + --footer-height: 184px; + } +} +/* Peers List */ + +#x-peers-filler { + display: flex; + flex-grow: 1; +} + +x-peers { + position: relative; + display: flex; + flex-flow: row wrap; + flex-grow: 1; + align-items: start !important; + justify-content: center; + + z-index: 2; + transition: --bg-color 0.5s ease; + overflow-y: scroll; + overflow-x: hidden; + overscroll-behavior-x: none; + scrollbar-width: none; + + --peers-per-row: 6; /* default if browser does not support :has selector */ + --x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px)); + width: var(--x-peers-width); + margin-right: 20px; + margin-left: 20px; +} + +x-peers.overflowing { + background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)), + linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%, + /* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)), + radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%; + + background-repeat: no-repeat; + background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +x-peers:has(> x-peer) { + --peers-per-row: 10; +} + +/* peers-per-row if height is too small for 2 rows */ +@media screen and (min-height: 538px) and (max-height: 683px) and (max-width: 402px), +screen and (min-height: 517px) and (max-height: 664px) and (max-width: 426px), +screen and (min-height: 501px) and (max-height: 647px) and (min-width: 426px) { + x-peers:has(> x-peer) { + --peers-per-row: 3; + } + + x-peers:has(> x-peer:nth-of-type(7)) { + --peers-per-row: 4; + } + + x-peers:has(> x-peer:nth-of-type(10)) { + --peers-per-row: 5; + } + + x-peers:has(> x-peer:nth-of-type(13)) { + --peers-per-row: 6; + } + + x-peers:has(> x-peer:nth-of-type(16)) { + --peers-per-row: 7; + } + + x-peers:has(> x-peer:nth-of-type(19)) { + --peers-per-row: 8; + } + + x-peers:has(> x-peer:nth-of-type(22)) { + --peers-per-row: 9; + } + + x-peers:has(> x-peer:nth-of-type(25)) { + --peers-per-row: 10; + } +} + +/* peers-per-row if height is too small for 3 rows */ +@media screen and (min-height: 683px) and (max-width: 402px), +screen and (min-height: 664px) and (max-width: 426px), +screen and (min-height: 647px) and (min-width: 426px) { + x-peers:has(> x-peer) { + --peers-per-row: 3; + } + + x-peers:has(> x-peer:nth-of-type(10)) { + --peers-per-row: 4; + } + + x-peers:has(> x-peer:nth-of-type(13)) { + --peers-per-row: 5; + } + + x-peers:has(> x-peer:nth-of-type(16)) { + --peers-per-row: 6; + } + + x-peers:has(> x-peer:nth-of-type(19)) { + --peers-per-row: 7; + } + + x-peers:has(> x-peer:nth-of-type(22)) { + --peers-per-row: 8; + } + + x-peers:has(> x-peer:nth-of-type(25)) { + --peers-per-row: 9; + } + + x-peers:has(> x-peer:nth-of-type(28)) { + --peers-per-row: 10; + } +} + +::-webkit-scrollbar { + display: none; +} + +/* Empty Peers List */ + +x-no-peers { + display: flex; + flex-direction: column; + padding: 8px; + text-align: center; + animation: fade-in 300ms; + animation-fill-mode: backwards; + /* prevent flickering on load */ + animation-iteration-count: 0; +} + +x-no-peers h2, +x-no-peers a { + color: var(--primary-color); + margin-bottom: 5px; +} + +x-peers:not(:empty)+x-no-peers { + display: none; +} + +x-no-peers::before { + color: var(--primary-color); + font-size: 24px; + font-weight: 400; + letter-spacing: -.012em; + line-height: 32px; +} + +x-no-peers[drop-bg]::before { + content: "Release to select recipient"; +} + +x-no-peers[drop-bg] * { + display: none; +} + + + +/* Peer */ + +x-peer { + padding: 8px; + align-content: start; + flex-wrap: wrap; +} + +x-peer label { + width: var(--peer-width); + cursor: pointer; + touch-action: manipulation; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + position: relative; +} + +input[type="file"] { + visibility: hidden; + position: absolute; +} + +x-peer x-icon { + --icon-size: 40px; + margin-bottom: 4px; + transition: transform 150ms; + will-change: transform; + display: flex; + flex-direction: column; +} + +x-peer .icon-wrapper { + width: var(--icon-size); + padding: 12px; + border-radius: 50%; + background: var(--primary-color); + color: white; + display: flex; +} + +x-peer:not(.type-ip).type-secret .icon-wrapper { + background: var(--paired-device-color); +} + +x-peer x-icon > .highlight-wrapper { + align-self: center; + align-items: center; + margin: 7px auto 0; + height: 6px; +} + +x-peer x-icon > .highlight-wrapper > .highlight { + width: 6px; + height: 6px; + border-radius: 50%; + display: none; +} + +x-peer.type-secret x-icon > .highlight-wrapper > .highlight { + background-color: var(--paired-device-color); + display: inline; +} + +x-peer:not([status]):hover x-icon, +x-peer:not([status]):focus x-icon { + transform: scale(1.05); +} + +x-peer[status] x-icon { + box-shadow: none; + opacity: 0.8; + transform: scale(1); +} + + +x-peer.ws-peer { + margin-top: -1.5px; +} + +x-peer.ws-peer .progress { + margin-top: 3px; +} + +x-peer.ws-peer .icon-wrapper{ + border: solid 3px var(--ws-peer-color); +} + +x-peer.ws-peer .highlight-wrapper { + margin-top: 3px; +} + +.device-descriptor { + width: 100%; + text-align: center; +} + +.device-descriptor > div { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} + +.status, +.device-name, +.connection-hash { + opacity: 0.7; +} + +.device-name { + font-size: 14px; + white-space: nowrap; +} + +.connection-hash { + font-size: 12px; + white-space: nowrap; +} + +x-peer[status=transfer] .status:before { + content: 'Transferring...'; +} + +x-peer[status=prepare] .status:before { + content: 'Preparing...'; +} + +x-peer[status=wait] .status:before { + content: 'Waiting...'; +} + +x-peer[status=process] .status:before { + content: 'Processing...'; +} + +x-peer:not([status]) .status, +x-peer[status] .device-name { + display: none; +} + +x-peer[status] { + pointer-events: none; +} + +x-peer x-icon { + animation: pop 600ms ease-out 1; +} + +@keyframes pop { + 0% { + transform: scale(0.7); + } + + 40% { + transform: scale(1.2); + } +} + +x-peer[drop] x-icon { + transform: scale(1.1); +} + + + +/* Footer */ + +footer { + position: relative; + margin-top: auto; + z-index: 2; + align-items: center; + text-align: center; + transition: color 300ms; + cursor: default; +} + +footer .logo { + --icon-size: 80px; + margin-bottom: 8px; + color: var(--primary-color); + margin-top: -10px; +} + +footer .font-body2 { + color: var(--primary-color); + margin: auto 18px; +} + +#on-this-network { + border-bottom: solid 4px var(--primary-color); + padding-bottom: 1px; +} + +#paired-devices { + border-bottom: solid 4px var(--paired-device-color); + padding-bottom: 1px; +} + +#display-name { + display: inline-block; + text-align: left; + border: none; + outline: none; + max-width: 15em; + text-overflow: ellipsis; + white-space: nowrap; + cursor: text; + margin-left: -1rem; + margin-bottom: -6px; + padding-right: 0.3rem; + padding-left: 0.3em; + padding-bottom: 0.1rem; + border-radius: 1.3rem/30%; + border-right: solid 1rem transparent; + border-left: solid 1rem transparent; + background-clip: padding-box; + background-color: rgba(var(--text-color), 43%); + color: white; + transition: background-color 0.5s ease; + overflow: hidden; +} + +#edit-pen { + width: 1rem; + height: 1rem; + margin-left: -1rem; + margin-bottom: -2px; + position: relative; + z-index: -1; +} + +/* Dialog */ + +x-dialog x-background { + background: rgba(0, 0, 0, 0.61); + z-index: 10; + transition: opacity 300ms; + will-change: opacity; + padding: 15px; + overflow: overlay; +} + +x-dialog x-paper { + z-index: 3; + background: white; + border-radius: 8px; + padding: 16px 24px; + width: 100%; + max-width: 400px; + overflow: hidden; + box-sizing: border-box; + transition: transform 300ms; + will-change: transform; +} + +#pair-device-dialog x-paper { + display: flex; + flex-direction: column; + position: absolute; + top: max(50%, 350px); + margin-top: -328.5px; + width: calc(100vw - 20px); + height: 625px; +} + +#pair-device-dialog ::-moz-selection, +#pair-device-dialog ::selection { + color: black; + background: var(--paired-device-color); +} + +x-dialog:not([show]) { + pointer-events: none; +} + +x-dialog:not([show]) x-paper { + transform: scale(0.1); +} + +x-dialog:not([show]) x-background { + opacity: 0; +} + + +x-dialog a { + color: var(--primary-color); +} + +x-dialog .font-subheading { + margin-bottom: 5px; +} + +/* Pair Devices Dialog */ + +#key-input-container { + width: 100%; + display: flex; + justify-content: center; +} + +#key-input-container > input { + width: 45px; + height: 45px; + font-size: 30px; + padding: 0; + text-align: center; + display: -webkit-box !important; + display: -webkit-flex !important; + display: -moz-flex !important; + display: -ms-flexbox !important; + display: flex !important; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; +} + +#key-input-container > input + * { + margin-left: 6px; +} + +#key-input-container > input:nth-of-type(4) { + margin-left: 5%; +} + +#room-key { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + font-size: 50px; + letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); + display: inline-block; + text-indent: calc(0.5 * (11px + min(calc((100vw - 80px - 99px) / 100 * 6), 23px))); + margin: 15px -15px; +} + +#room-key-qr-code { + margin: 16px; +} + +x-dialog hr { + margin: 40px -24px 30px -24px; + border: solid 1.25px var(--border-color); +} + +#pair-device-dialog x-background { + padding: 16px!important; +} + +/* Edit Paired Devices Dialog */ +.paired-devices-wrapper:empty:before { + content: "No paired devices."; +} + +.paired-devices-wrapper:empty { + padding: 10px; +} + +.paired-devices-wrapper { + border-top: solid 4px var(--paired-device-color); + border-bottom: solid 4px var(--paired-device-color); + max-height: 65vh; + overflow: scroll; + background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)), + linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%, + /* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)), + radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%; + + background-repeat: no-repeat; + background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +.paired-device { + display: flex; + justify-content: space-between; + flex-direction: column; + align-items: center; +} + +.paired-device:not(:last-child) { + border-bottom: solid 4px var(--paired-device-color); +} + +.paired-device > .display-name, +.paired-device > .device-name { + width: 100%; + height: 36px; + display: flex; + align-items: center; + text-align: center; + align-self: center; + border-bottom: solid 2px rgba(128, 128, 128, 0.5); + opacity: 1; +} +.paired-device span { + width: 100%; +} + +.paired-device > .button-wrapper { + display: flex; + height: 36px; + justify-content: space-between; + flex-direction: row; + align-items: center; + width: 100%; +} + +.paired-device > .button-wrapper > label, +.paired-device > .button-wrapper > button { + display: flex; + align-items: center; + text-align: center; + white-space: nowrap; + justify-content: center; + width: 50%; + padding-left: 6px; + padding-right: 6px; + height: 36px; +} + +.paired-device > .button-wrapper > :not(:last-child) { + border-right: solid 1px rgba(128, 128, 128, 0.5); +} + +.paired-device > .button-wrapper > :not(:first-child) { + border-left: solid 1px rgba(128, 128, 128, 0.5); +} + +.paired-device * { + overflow: hidden; + text-overflow: ellipsis; +} + +.paired-device > .auto-accept { + cursor: pointer; +} + +/* Receive Dialog */ + +x-dialog .row { + margin-top: 24px; + margin-bottom: 8px; +} + +/* button row*/ +x-paper > div:last-child { + margin: auto -24px -15px; + border-top: solid 2.5px var(--border-color); + height: 50px; +} + +x-paper > div:last-child > .button { + height: 100%; + width: 100%; +} + +x-paper > div:last-child > .button:not(:last-child) { + border-left: solid 2.5px var(--border-color); +} + +.file-description { + margin-bottom: 25px; +} + +.file-description .row { + margin: 0 +} + +.file-description span { + display: inline; + word-break: normal; +} + +.file-name { + font-style: italic; + max-width: 100%; +} + +.file-stem { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Send Text Dialog */ +/* Todo: add pair underline to send / receive dialogs displayName */ +x-dialog .dialog-subheader { + margin-bottom: 25px; +} + +#text-input { + min-height: 200px; + margin: 14px auto; +} + +/* Receive Text Dialog */ + +#receive-text-dialog #text { + width: 100%; + word-break: break-all; + max-height: calc(100vh - 393px); + overflow-x: hidden; + overflow-y: auto; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + white-space: pre-wrap; + padding: 15px 0; +} + +#receive-text-dialog #text a { + cursor: pointer; +} + +#receive-text-dialog #text a:hover { + text-decoration: underline; +} + +#receive-text-dialog h3 { + /* Select the received text when double-clicking the dialog */ + user-select: none; + pointer-events: none; +} + +.row-separator { + border-bottom: solid 2.5px var(--border-color); + margin: auto -24px; +} + +#base64-paste-btn, +#base64-paste-dialog .textarea { + width: 100%; + height: 40vh; + border: solid 12px #438cff; + text-align: center; +} + +#base64-paste-dialog .textarea { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +#base64-paste-dialog .textarea::before { + font-size: 15px; + letter-spacing: 0.12em; + color: var(--primary-color); + font-weight: 700; + text-transform: uppercase; + content: attr(placeholder); +} + +#base64-paste-dialog button { + margin: auto; + border-radius: 8px; +} + +#base64-paste-dialog button[close] { + margin-top: 20px; +} + +#base64-paste-dialog button[close]:before { + border-radius: 8px; +} + +/* Button */ + +.button { + padding: 2px 16px 0; + box-sizing: border-box; + min-height: 36px; + font-size: 14px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + white-space: nowrap; + cursor: pointer; + user-select: none; + background: inherit; + color: var(--primary-color); + overflow: hidden; +} + +.button[disabled] { + color: #5B5B66; +} + + +.button, +.icon-button { + position: relative; + display: flex; + align-items: center; + justify-content: center; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + touch-action: manipulation; + border: none; + outline: none; +} + +.button:before, +.icon-button:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: currentColor; + opacity: 0; + transition: opacity 300ms; +} + +.button:not([disabled]):hover:before, +.icon-button:hover:before { + opacity: 0.1; +} + +#cancel-paste-mode { + z-index: 2; + margin: 0; + padding: 0; + position: absolute; + top: 0; + right: 0; + left: 0; + width: 100vw; + height: 56px; + background-color: var(--primary-color); + color: rgb(238, 238, 238); +} + +.button:focus:before, +.icon-button:focus:before { + opacity: 0.2; +} + + + +button::-moz-focus-inner { + border: 0; +} + + +/* Icon Button */ +.icon-button { + width: 40px; + height: 40px; +} + +.icon-button:before { + border-radius: 50%; +} + +/* Text Input */ +.textarea { + box-sizing: border-box; + border: none; + outline: none; + padding: 16px 24px; + border-radius: 16px; + margin: 10px 0; + font-size: 14px; + font-family: inherit; + background: #f1f3f4; + display: block; + overflow: auto; + resize: none; + line-height: 16px; + max-height: calc(100vh - 254px); + white-space: pre; +} + + +/* Info Animation */ + +#about { + color: white; + z-index: 11; + overflow: hidden; + pointer-events: none; + text-align: center; +} + +#about .fade-in { + transition: opacity 300ms; + will-change: opacity; + transition-delay: 300ms; + z-index: 11; + pointer-events: all; +} + +#about:not(:target) .fade-in { + opacity: 0; + pointer-events: none; + transition-delay: 0s; +} + +#about .logo { + --icon-size: 96px; +} + +#about .title-wrapper { + display: flex; + align-items: baseline; +} + +#about .title-wrapper > div { + margin-left: 0.5em; +} + +#about x-background { + position: absolute; + --size: max(max(230vw, 230vh), calc(150vh + 150vw)); + --size-half: calc(var(--size)/2); + top: calc(28px - var(--size-half)); + right: calc(36px - var(--size-half)); + width: var(--size); + height: var(--size); + border-radius: 50%; + background: var(--primary-color); + transform: scale(0); + z-index: -1; +} + +/* Hack such that initial scale(0) isn't animated */ +#about x-background { + will-change: transform; + transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1); +} + +#about:target x-background { + transform: scale(1); +} + +#about .row a { + margin: 8px 8px -16px; +} + +#about section { + flex-grow: 1; +} + +#about header { + align-self: end; +} + +canvas.circles { + width: 100vw; + position: absolute; + z-index: -10; + top: 0; + left: 0; +} + +/* Loading Indicator */ + +.progress { + width: 80px; + height: 80px; + position: absolute; + top: -8px; + clip: rect(0px, 80px, 80px, 40px); + --progress: rotate(0deg); + transition: transform 200ms; +} + +.circle { + width: 72px; + height: 72px; + border: 4px solid var(--primary-color); + border-radius: 40px; + position: absolute; + clip: rect(0px, 40px, 80px, 0px); + will-change: transform; + transform: var(--progress); +} + +.over50 { + clip: rect(auto, auto, auto, auto); +} + +.over50 .circle.right { + transform: rotate(180deg); +} + + +/* Generic placeholder */ +[placeholder]:empty:before { + content: attr(placeholder); +} + +/* Toast */ + +.toast-container { + padding: 0 8px 24px; + overflow: hidden; + pointer-events: none; +} + +x-toast { + position: absolute; + min-height: 48px; + top: 50px; + width: 100%; + max-width: 344px; + background-color: rgb(var(--text-color)); + color: rgb(var(--bg-color)); + align-items: center; + box-sizing: border-box; + padding: 8px 24px; + z-index: 20; + transition: opacity 200ms, transform 300ms ease-out; + cursor: default; + line-height: 24px; + border-radius: 8px; + pointer-events: all; +} + +x-toast:not([show]):not(:hover) { + opacity: 0; + transform: translateY(-100px); +} + + +/* Instructions */ + +x-instructions { + position: relative; + opacity: 0.5; + transition: opacity 300ms; + text-align: center; + margin-left: 10px; + margin-right: 10px; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; +} + +x-instructions:not([drop-peer]):not([drop-bg]):before { + content: attr(mobile); +} + +x-instructions[drop-peer]:before { + content: "Release to send to peer"; +} + +x-instructions[drop-bg]:not([drop-peer]):before { + content: "Release to select recipient"; +} + +x-instructions p { + display: none; +} + +x-peers:empty~x-instructions { + opacity: 0; +} + +@media (hover: none) and (pointer: coarse) { + x-peer { + transform: scale(0.95); + padding: 4px; + } +} + +#websocket-fallback { + margin-left: 5px; + margin-right: 5px; + padding: 5px; + text-align: center; + opacity: 0.5; + transition: opacity 300ms; +} + +#websocket-fallback > span { + margin: 2px; +} + +#websocket-fallback > span > span { + border-bottom: solid 4px var(--ws-peer-color); +} + +/* Responsive Styles */ +@media screen and (max-width: 360px) { + x-dialog x-paper { + padding: 15px; + } + x-paper > div:last-child { + margin: auto -15px -15px; + } +} + +@media screen and (min-height: 800px) { + #websocket-fallback { + padding-bottom: 16px; + } +} + +@media (hover: hover) and (pointer: fine) { + x-instructions:not([drop-peer]):not([drop-bg]):before { + content: attr(desktop); + } +} + +/* + Color Themes +*/ + +/* Default colors */ +body { + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ + --bg-color-test: 18,18,18; + --bg-color-secondary: #f1f3f4; + --border-color: #e7e8e8; +} + +/* Dark theme colors */ +body.dark-theme { + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ + --bg-color-secondary: #333; + --border-color: #252525; +} + +/* Colored Elements */ +body { + color: rgb(var(--text-color)); + background-color: rgb(var(--bg-color)); + transition: background-color 0.5s ease; +} + +x-dialog x-paper { + background-color: rgb(var(--bg-color)); +} + +.textarea { + color: rgb(var(--text-color)) !important; + background-color: var(--bg-color-secondary) !important; +} + +.textarea * { + margin: 0 !important; + padding: 0 !important; + color: unset !important; + background: unset !important; + border: unset !important; + opacity: unset !important; + font-family: inherit !important; + font-size: inherit !important; + font-style: unset !important; + font-weight: unset !important; +} + +/* Image/Video/Audio Preview */ +.file-preview { + margin: 10px -24px 40px -24px; +} + +.file-preview:empty { + display: none; +} + +.file-preview > img, +.file-preview > audio, +.file-preview > video { + max-width: 100%; + max-height: 40vh; + margin: auto; + display: block; +} + +/* Styles for users who prefer dark mode at the OS level */ +@media (prefers-color-scheme: dark) { + + /* defaults to dark theme */ + body { + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ + --bg-color-secondary: #333; + --border-color: #252525; + } + + /* Override dark mode with light mode styles if the user decides to swap */ + body.light-theme { + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ + --bg-color-secondary: #f1f3f4; + --border-color: #e7e8e8; + } +} + + +/* + Edge specific styles +*/ +@supports (-ms-ime-align: auto) { + + html, + body { + overflow: hidden; + } +} + +/* + iOS specific styles +*/ +@supports (-webkit-overflow-scrolling: touch) { + html { + min-height: -webkit-fill-available; + } +} + +/* webkit scrollbar style*/ + +::-webkit-scrollbar{ + width: 4px; + height: 4px; +} + +::-webkit-scrollbar-thumb{ + background: #bfbfbf; + border-radius: 4px; +} + +::-moz-selection, +::selection { + color: black; + background: var(--primary-color); +} diff --git a/rtc_config_example.json b/rtc_config_example.json new file mode 100644 index 0000000..d7e48e8 --- /dev/null +++ b/rtc_config_example.json @@ -0,0 +1,13 @@ +{ + "sdpSemantics": "unified-plan", + "iceServers": [ + { + "urls": "stun:stun.l.google.com:19302" + }, + { + "urls": "turn:example.com:3478", + "username": "username", + "credential": "password" + } + ] +} diff --git a/turnserver_example.conf b/turnserver_example.conf new file mode 100644 index 0000000..60e2d8a --- /dev/null +++ b/turnserver_example.conf @@ -0,0 +1,38 @@ +# TURN server name and realm +realm= +server-name=moreload + +# IPs the TURN server listens to +listening-ip=0.0.0.0 + +# External IP-Address of the TURN server +external-ip= + +# Main listening port +listening-port=3478 + +# Further ports that are open for communication +min-port=10000 +max-port=20000 + +# Use fingerprint in TURN message +fingerprint + +# Log file path +log-file=/var/log/turnserver.log + +# Enable verbose logging +verbose + +# Specify the user for the TURN authentification +user=user:password + +# Enable long-term credential mechanism +lt-cred-mech + +# SSL certificates +cert=/etc/letsencrypt/live//cert.pem +pkey=/etc/letsencrypt/live//privkey.pem + +# 443 for TURN over TLS, which can bypass firewalls +tls-listening-port=443 diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..16152ca --- /dev/null +++ b/yarn.lock @@ -0,0 +1,431 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express-rate-limit@^6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-6.7.0.tgz#6aa8a1bd63dfe79702267b3af1161a93afc1d3c2" + integrity sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA== + +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-intrinsic@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +ua-parser-js@^1.0.35: + version "1.0.35" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011" + integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA== + +unique-names-generator@^4.3.0: + version "4.7.1" + resolved "https://registry.yarnpkg.com/unique-names-generator/-/unique-names-generator-4.7.1.tgz#966407b12ba97f618928f77322cfac8c80df5597" + integrity sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +ws@^8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==