diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..e297939f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,67 @@ +name: Analyze, build and distribute Android application + +on: + push: + branches: + - master + +env: + flutter_version: "1.22.5" + +jobs: + buildApk: + name: Analyze, build and distribute Android application + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v1 + - name: Set up Java + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache Flutter dependencies + uses: actions/cache@v1 + with: + path: /opt/hostedtoolcache/flutter + key: ${{ runner.OS }}-flutter-install-cache-${{ env.flutter_version }} + - name: Set up flutter + uses: subosito/flutter-action@v1 + with: + flutter-version: ${{ env.flutter_version }} + - name: Get dependencies + run: flutter pub get + - name: Format source code + run: find -name "*.dart" -not -path "./lib/generated/*" -exec flutter format --set-exit-if-changed {} \; + - name: Analyze source code + run: flutter analyze . + - name: Add config files + env: + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + GOOGLE_SERVICES_STAGE_JSON: ${{ secrets.GOOGLE_SERVICES_STAGE_JSON }} + GOOGLE_SERVICES_PROD_JSON: ${{ secrets.GOOGLE_SERVICES_PROD_JSON }} + DEBUG_JKS: ${{ secrets.DEBUG_JKS }} + RELEASE_JKS: ${{ secrets.RELEASE_JKS }} + run: | + sudo echo "$KEYSTORE_PROPERTIES" > android/keystore.properties + sudo echo $GOOGLE_SERVICES_STAGE_JSON > android/app/src/stage/google-services.json + sudo echo $GOOGLE_SERVICES_PROD_JSON > android/app/src/prod/google-services.json + sudo echo "$DEBUG_JKS" | base64 -d > android/debug.jks + sudo echo "$RELEASE_JKS" | base64 -d > android/release.jks + - name: Build stage application + run: flutter build apk --flavor stage + - name: Build production application + run: flutter build apk --flavor prod + - name: Distribute stage via Firebase + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{ secrets.STAGE_APP_ID }} + token: ${{ secrets.FIREBASE_TOKEN }} + groups: testers + file: build/app/outputs/flutter-apk/app-stage-release.apk + - name: Distribute production via Firebase + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{ secrets.PROD_APP_ID }} + token: ${{ secrets.FIREBASE_TOKEN }} + groups: testers + file: build/app/outputs/flutter-apk/app-prod-release.apk diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..71ea3119 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +/fastlane/service_account.json diff --git a/.metadata b/.metadata new file mode 100644 index 00000000..911b6b72 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 81a45ec2e5f80fa71d5135f1702ce540558b416d + channel: beta + +project_type: app diff --git a/.run/Dev.run.xml b/.run/Dev.run.xml new file mode 100644 index 00000000..8e84394c --- /dev/null +++ b/.run/Dev.run.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/.run/song_checker.dart.run.xml b/.run/song_checker.dart.run.xml new file mode 100644 index 00000000..0071c77f --- /dev/null +++ b/.run/song_checker.dart.run.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.run/users_mitigator.dart.run.xml b/.run/users_mitigator.dart.run.xml new file mode 100644 index 00000000..c2e76b85 --- /dev/null +++ b/.run/users_mitigator.dart.run.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f288702d --- /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 00000000..ca9608b8 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Hit Notes - Play instruments + +![Analyze, build and distribute Android application](https://github.com/cuong0993/HitNotes/workflows/Analyze,%20build%20and%20distribute%20Android%20application/badge.svg) + +Rhythm-based mobile game. Even you don't have any basic knowledge of music instruments, you still can play with falling notes in the game! ✨ + +Currently available on the Play store. + +Get it on Google Play + +# Main Features + +- Hundreds of songs, from classics of Beethoven, Chopin, Mozart, or Schubert, folk songs to POP/EDM. Will be updated continuously +- Adjustable difficulty level +- Adjustable tempo +- Many game modes +- Many musical instruments +- Play offline without the internet +- Upload your song and play your way + +![screenshots](./fastlane/metadata/en-US/images/phoneScreenshots/1_en-US.png) +![screenshots](./fastlane/metadata/en-US/images/phoneScreenshots/2_en-US.png) +![screenshots](./fastlane/metadata/en-US/images/phoneScreenshots/3_en-US.png) + +# Project Structure + +This is a Flutter mobile game targeting Android and iOS. + +The code for the Flutter app is contained in the folder `lib` and the +different native apps are in `android` and `ios`. + +Extra project assets are in `assets`. + +Firebase config files and cloud functions are in the `firebase` folder. + +Google Play store listing files are in the `fastlane` folder. + +# Prerequisites & Getting Started + +## Client + +To build and run the mobile apps you’ll need to install [Flutter](https://flutter.dev) and its dependencies. To verify your installation run in the project’s root directory:**‌** + +``` +$ flutter doctor +``` + +## Backend (Firebase) + +The backend is build using Firebase’s `node.js` SDK. All files are provided in the `firebase` folder. To deploy the code create an app and install the firebase CLI (See steps 1 & 2 in [Getting started](https://firebase.google.com/docs/functions/get-started)). + +### B1. Setup sign-in method +An initial sign-in method needs to be configured. + +- Select your project in [console.firebase.google.com](https://console.firebase.google.com). +- Navigate to `Authentication` +- Select `Sign-in methods` and activate `Google`, `Facebook` and `Anonymous`. + +### B2. Configure firebase app + +Next, you’ll need to configure your firebase app for Flutter as described in [Add Firebase to an App / Flutter](https://firebase.google.com/docs/flutter/setup) + +**Android** + +Follow the instructions in `android/README.md`. + +### B3. Deploy firebase functions + +Navigate to the `firebase` directory and deploy all functions and configuration using: + +``` +$ firebase deploy --project={projectId} +``` + +### B4. Synchronize storage + +Install Google Cloud SDK, navigate to the `storage` directory and synchronize storage files using: + +``` +$ gsutil -m rsync -r -d ./ gs://{projectId}.appspot.com +``` + +### B4. Synchronize database + +Install https://github.com/jloosli/node-firestore-import-export, get service account json file https://firebase.google.com/docs/admin/setup#initialize-sdk, navigate to the `database` directory, backup/restore database using: + +``` +$ export GOOGLE_APPLICATION_CREDENTIALS="service-account.json"; ./backup-firestore.sh +$ export GOOGLE_APPLICATION_CREDENTIALS="service-account.json"; ./restore-firestore.sh +``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..435dbb1b --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,97 @@ +# Pedantic package: The Dart analyzer settings and best practices used internally at Google. +include: package:pedantic/analysis_options.yaml + +# Analysis from Flutter stable channel +# https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/analysis_options_user.yaml + +# Specify analysis options. +# +# Until there are meta linter rules, each desired lint must be explicitly enabled. +# See: https://github.com/dart-lang/linter/issues/288 +# +# For a list of lints, see: http://dart-lang.github.io/linter/lints/ +# See the configuration guide for more +# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer +# +# There are four similar analysis options files in the flutter repos: +# - analysis_options.yaml +# - packages/flutter/lib/analysis_options_user.yaml (this file) +# - https://github.com/flutter/plugins/blob/master/analysis_options.yaml +# - https://github.com/flutter/engine/blob/master/analysis_options.yaml +# +# This file contains the analysis options used by "flutter analyze" and the +# dartanalyzer when analyzing code outside the flutter repository. It isn't named +# 'analysis_options.yaml' because otherwise editors would use it when analyzing +# the flutter tool itself. +# +# When editing, make sure you keep this and /analysis_options.yaml consistent. + +analyzer: + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: error + omit_local_variable_types: error + include_file_not_found: error + unused_element: error + unused_field: error + unused_local_variable: error + exclude: # excluded generated files + - lib/songs/song.dart + - lib/game/tile/tile_chunk.dart + - lib/game/note/note.dart + - lib/generated/** + +linter: + rules: + # these rules are documented on and in the same order as + # the Dart Lint rules page to make maintenance easier + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + # - always_specify_types + - annotate_overrides + # - avoid_as + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_types + - cancel_subscriptions + - close_sinks + # - comment_references # we do not presume as to what people want to reference in their dartdocs + # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 + - control_flow_in_finally + - empty_statements + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - library_prefixes + - list_remove_unrelated_type + - literal_only_boolean_expressions + - non_constant_identifier_names + # - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_mixin # https://github.com/dart-lang/language/issues/32 + # - public_member_api_docs + # - sort_constructors_first + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis # subset of always_specify_types + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_getters_setters + - unnecessary_statements + + # The below linters are added manually + - directives_ordering + - file_names + # - lines_longer_than_80_chars + #- public_member_api_docs + - prefer_final_in_for_each + # - sort_constructors_first + # - avoid_as + - prefer_final_locals + - prefer_relative_imports diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..9cf0b64f --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +keystore.properties +debug.jks +release.jks +google-services.json \ No newline at end of file diff --git a/android/README.md b/android/README.md new file mode 100644 index 00000000..7a07d7d6 --- /dev/null +++ b/android/README.md @@ -0,0 +1,35 @@ +# Android client + +## Setting up Firebase + +Create a project on the Firebase console [here](https://console.firebase.google.com/) + +1. To add Firebase to your app, click on the android icon or click the gear icon to go to project +settings to find the android icon. + +2. Register your application by filing up the form with the package name (applicationId) +and the app nickname if you like. +> Find Your package name which is generally the applicationId in your app-level build.gradle file + +3. Download the `google-service.json` file that is generated for you. Find it and move it inside +the folder `android/app/` of the project. The firebase sdk is already added to the project. + +4. On the fourth step of registration, run the app to verify the configuration via the Firebase +console. + +## Distribution + +To build this application for distribution, +provide files `debug.jks` and `release.jks` containing the signing keys, +and the `key.properties` with the following content: + +``` +debugStorePassword=..... +debugKeyPassword=..... +debugKeyAlias=..... +debugStoreFile=../debug.jks +releaseStorePassword=..... +releaseKeyPassword=..... +releaseKeyAlias=..... +releaseStoreFile=../release.jks +``` diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..189e3806 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,98 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('keystore.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +apply plugin: 'com.google.gms.google-services' +apply plugin: 'com.google.firebase.crashlytics' +android { + compileSdkVersion 30 + ndkVersion "22.0.7026061" + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "com.chaomao.hittick" + minSdkVersion 16 + targetSdkVersion 30 + multiDexEnabled true + versionCode 22 + versionName "1.2.3" + } + + signingConfigs { + debug { + keyAlias keystoreProperties['debugKeyAlias'] + keyPassword keystoreProperties['debugKeyPassword'] + storeFile keystoreProperties['debugStoreFile'] ? file(keystoreProperties['debugStoreFile']) : null + storePassword keystoreProperties['debugStorePassword'] + } + release { + keyAlias keystoreProperties['releaseKeyAlias'] + keyPassword keystoreProperties['releaseKeyPassword'] + storeFile keystoreProperties['releaseStoreFile'] ? file(keystoreProperties['releaseStoreFile']) : null + storePassword keystoreProperties['releaseStorePassword'] + } + } + + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + signingConfig signingConfigs.release + } + } + + variantFilter { variant -> + def names = variant.flavors*.name + if (variant.buildType.name == 'release' && names.contains("dev")) { + setIgnore(true) + } + } + + flavorDimensions "default" + productFlavors { + dev { + } + + stage { + } + + prod { + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:multidex:1.0.3' + implementation platform('com.google.firebase:firebase-bom:26.1.0') +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..29f9e597 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/android/app/src/debug/res/values/strings_no_translate.xml b/android/app/src/debug/res/values/strings_no_translate.xml new file mode 100644 index 00000000..f709eefc --- /dev/null +++ b/android/app/src/debug/res/values/strings_no_translate.xml @@ -0,0 +1,4 @@ + + + Hit Notes Dev + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8ecea145 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/chaomao/hitnotes/MainActivity.kt b/android/app/src/main/kotlin/com/chaomao/hitnotes/MainActivity.kt new file mode 100644 index 00000000..e722e1d1 --- /dev/null +++ b/android/app/src/main/kotlin/com/chaomao/hitnotes/MainActivity.kt @@ -0,0 +1,5 @@ +package com.chaomao.hitnotes + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..929a0a5e --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..8ae0e296 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..8ae0e296 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..00ed4098 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 00000000..0a7f3378 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..0f4d84d9 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..e655c7df Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/img_chaomao.png b/android/app/src/main/res/mipmap-hdpi/img_chaomao.png new file mode 100644 index 00000000..f31b4ac2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/img_chaomao.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 00000000..a065a5df Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png new file mode 100644 index 00000000..569be1b8 Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png new file mode 100644 index 00000000..f8d1e6b9 Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png new file mode 100644 index 00000000..713872cb Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/img_chaomao.png b/android/app/src/main/res/mipmap-ldpi/img_chaomao.png new file mode 100644 index 00000000..a19d0cae Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/img_chaomao.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..27728d54 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 00000000..ca2c558f Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..657d8fdc Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..581a3bf8 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/img_chaomao.png b/android/app/src/main/res/mipmap-mdpi/img_chaomao.png new file mode 100644 index 00000000..2fe3a572 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/img_chaomao.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..543c467d Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 00000000..44361d40 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a537559a Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..e03eb158 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/img_chaomao.png b/android/app/src/main/res/mipmap-xhdpi/img_chaomao.png new file mode 100644 index 00000000..7c4b2161 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/img_chaomao.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..b8864f32 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..6719cf6b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..1a83e6ae Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..fe4cec35 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/img_chaomao.png b/android/app/src/main/res/mipmap-xxhdpi/img_chaomao.png new file mode 100644 index 00000000..ce4cbbec Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/img_chaomao.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..068fca23 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..c27695e3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..d585086e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..2cb7c961 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/img_chaomao.png b/android/app/src/main/res/mipmap-xxxhdpi/img_chaomao.png new file mode 100644 index 00000000..50c426ef Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/img_chaomao.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..d8862059 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #000000 + diff --git a/android/app/src/main/res/values/strings_no_translate.xml b/android/app/src/main/res/values/strings_no_translate.xml new file mode 100644 index 00000000..80125b10 --- /dev/null +++ b/android/app/src/main/res/values/strings_no_translate.xml @@ -0,0 +1,4 @@ + + + 134284937214878 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..44b07882 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/android/app/src/prod/res/values/strings_no_translate.xml b/android/app/src/prod/res/values/strings_no_translate.xml new file mode 100644 index 00000000..35679e33 --- /dev/null +++ b/android/app/src/prod/res/values/strings_no_translate.xml @@ -0,0 +1,4 @@ + + + Hit Notes + diff --git a/android/app/src/stage/res/values/strings_no_translate.xml b/android/app/src/stage/res/values/strings_no_translate.xml new file mode 100644 index 00000000..35d145b4 --- /dev/null +++ b/android/app/src/stage/res/values/strings_no_translate.xml @@ -0,0 +1,4 @@ + + + Hit Notes Stage + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..17211732 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,33 @@ +buildscript { + ext.kotlin_version = '1.4.21' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.6.4' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.3.4' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..a6738207 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..bc24dcf0 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..44e62bcf --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/images/1.5x/img_app_icon.png b/assets/images/1.5x/img_app_icon.png new file mode 100644 index 00000000..585acb6a Binary files /dev/null and b/assets/images/1.5x/img_app_icon.png differ diff --git a/assets/images/1.5x/img_clef.png b/assets/images/1.5x/img_clef.png new file mode 100644 index 00000000..610b0ac3 Binary files /dev/null and b/assets/images/1.5x/img_clef.png differ diff --git a/assets/images/1.5x/img_clock.png b/assets/images/1.5x/img_clock.png new file mode 100644 index 00000000..dba38ef9 Binary files /dev/null and b/assets/images/1.5x/img_clock.png differ diff --git a/assets/images/1.5x/img_facebook.png b/assets/images/1.5x/img_facebook.png new file mode 100644 index 00000000..6674d57b Binary files /dev/null and b/assets/images/1.5x/img_facebook.png differ diff --git a/assets/images/1.5x/img_google.png b/assets/images/1.5x/img_google.png new file mode 100644 index 00000000..e2de7bf2 Binary files /dev/null and b/assets/images/1.5x/img_google.png differ diff --git a/assets/images/1.5x/img_guitar.png b/assets/images/1.5x/img_guitar.png new file mode 100644 index 00000000..b8c7751c Binary files /dev/null and b/assets/images/1.5x/img_guitar.png differ diff --git a/assets/images/1.5x/img_note.png b/assets/images/1.5x/img_note.png new file mode 100644 index 00000000..9c0abe0a Binary files /dev/null and b/assets/images/1.5x/img_note.png differ diff --git a/assets/images/1.5x/img_single_note.png b/assets/images/1.5x/img_single_note.png new file mode 100644 index 00000000..7577c712 Binary files /dev/null and b/assets/images/1.5x/img_single_note.png differ diff --git a/assets/images/1.5x/img_staff.png b/assets/images/1.5x/img_staff.png new file mode 100644 index 00000000..fb80e297 Binary files /dev/null and b/assets/images/1.5x/img_staff.png differ diff --git a/assets/images/1.5x/img_star.png b/assets/images/1.5x/img_star.png new file mode 100644 index 00000000..154e90ce Binary files /dev/null and b/assets/images/1.5x/img_star.png differ diff --git a/assets/images/1.5x/img_star_rate.png b/assets/images/1.5x/img_star_rate.png new file mode 100644 index 00000000..9aacb8d8 Binary files /dev/null and b/assets/images/1.5x/img_star_rate.png differ diff --git a/assets/images/1.5x/img_star_rate_disable.png b/assets/images/1.5x/img_star_rate_disable.png new file mode 100644 index 00000000..f396262f Binary files /dev/null and b/assets/images/1.5x/img_star_rate_disable.png differ diff --git a/assets/images/1.5x/img_touch.png b/assets/images/1.5x/img_touch.png new file mode 100644 index 00000000..dc443685 Binary files /dev/null and b/assets/images/1.5x/img_touch.png differ diff --git a/assets/images/2.0x/img_app_icon.png b/assets/images/2.0x/img_app_icon.png new file mode 100644 index 00000000..9dbe05c8 Binary files /dev/null and b/assets/images/2.0x/img_app_icon.png differ diff --git a/assets/images/2.0x/img_clef.png b/assets/images/2.0x/img_clef.png new file mode 100644 index 00000000..b897b7c2 Binary files /dev/null and b/assets/images/2.0x/img_clef.png differ diff --git a/assets/images/2.0x/img_clock.png b/assets/images/2.0x/img_clock.png new file mode 100644 index 00000000..32df4cb8 Binary files /dev/null and b/assets/images/2.0x/img_clock.png differ diff --git a/assets/images/2.0x/img_facebook.png b/assets/images/2.0x/img_facebook.png new file mode 100644 index 00000000..1ac26299 Binary files /dev/null and b/assets/images/2.0x/img_facebook.png differ diff --git a/assets/images/2.0x/img_google.png b/assets/images/2.0x/img_google.png new file mode 100644 index 00000000..e29e570c Binary files /dev/null and b/assets/images/2.0x/img_google.png differ diff --git a/assets/images/2.0x/img_guitar.png b/assets/images/2.0x/img_guitar.png new file mode 100644 index 00000000..54f43251 Binary files /dev/null and b/assets/images/2.0x/img_guitar.png differ diff --git a/assets/images/2.0x/img_note.png b/assets/images/2.0x/img_note.png new file mode 100644 index 00000000..6905a902 Binary files /dev/null and b/assets/images/2.0x/img_note.png differ diff --git a/assets/images/2.0x/img_single_note.png b/assets/images/2.0x/img_single_note.png new file mode 100644 index 00000000..c5ba1cd1 Binary files /dev/null and b/assets/images/2.0x/img_single_note.png differ diff --git a/assets/images/2.0x/img_staff.png b/assets/images/2.0x/img_staff.png new file mode 100644 index 00000000..2d142585 Binary files /dev/null and b/assets/images/2.0x/img_staff.png differ diff --git a/assets/images/2.0x/img_star.png b/assets/images/2.0x/img_star.png new file mode 100644 index 00000000..b6e816b9 Binary files /dev/null and b/assets/images/2.0x/img_star.png differ diff --git a/assets/images/2.0x/img_star_rate.png b/assets/images/2.0x/img_star_rate.png new file mode 100644 index 00000000..a4b8a478 Binary files /dev/null and b/assets/images/2.0x/img_star_rate.png differ diff --git a/assets/images/2.0x/img_star_rate_disable.png b/assets/images/2.0x/img_star_rate_disable.png new file mode 100644 index 00000000..27f3c62d Binary files /dev/null and b/assets/images/2.0x/img_star_rate_disable.png differ diff --git a/assets/images/2.0x/img_touch.png b/assets/images/2.0x/img_touch.png new file mode 100644 index 00000000..6ede133d Binary files /dev/null and b/assets/images/2.0x/img_touch.png differ diff --git a/assets/images/3.0x/img_app_icon.png b/assets/images/3.0x/img_app_icon.png new file mode 100644 index 00000000..5a5ebf85 Binary files /dev/null and b/assets/images/3.0x/img_app_icon.png differ diff --git a/assets/images/3.0x/img_clef.png b/assets/images/3.0x/img_clef.png new file mode 100644 index 00000000..a6474138 Binary files /dev/null and b/assets/images/3.0x/img_clef.png differ diff --git a/assets/images/3.0x/img_clock.png b/assets/images/3.0x/img_clock.png new file mode 100644 index 00000000..9b620947 Binary files /dev/null and b/assets/images/3.0x/img_clock.png differ diff --git a/assets/images/3.0x/img_facebook.png b/assets/images/3.0x/img_facebook.png new file mode 100644 index 00000000..b9a7b5b0 Binary files /dev/null and b/assets/images/3.0x/img_facebook.png differ diff --git a/assets/images/3.0x/img_google.png b/assets/images/3.0x/img_google.png new file mode 100644 index 00000000..161ef4c3 Binary files /dev/null and b/assets/images/3.0x/img_google.png differ diff --git a/assets/images/3.0x/img_guitar.png b/assets/images/3.0x/img_guitar.png new file mode 100644 index 00000000..62a4900e Binary files /dev/null and b/assets/images/3.0x/img_guitar.png differ diff --git a/assets/images/3.0x/img_note.png b/assets/images/3.0x/img_note.png new file mode 100644 index 00000000..5ce13047 Binary files /dev/null and b/assets/images/3.0x/img_note.png differ diff --git a/assets/images/3.0x/img_single_note.png b/assets/images/3.0x/img_single_note.png new file mode 100644 index 00000000..45b5ecf3 Binary files /dev/null and b/assets/images/3.0x/img_single_note.png differ diff --git a/assets/images/3.0x/img_staff.png b/assets/images/3.0x/img_staff.png new file mode 100644 index 00000000..0e27a160 Binary files /dev/null and b/assets/images/3.0x/img_staff.png differ diff --git a/assets/images/3.0x/img_star.png b/assets/images/3.0x/img_star.png new file mode 100644 index 00000000..275189b1 Binary files /dev/null and b/assets/images/3.0x/img_star.png differ diff --git a/assets/images/3.0x/img_star_rate.png b/assets/images/3.0x/img_star_rate.png new file mode 100644 index 00000000..81d763a9 Binary files /dev/null and b/assets/images/3.0x/img_star_rate.png differ diff --git a/assets/images/3.0x/img_star_rate_disable.png b/assets/images/3.0x/img_star_rate_disable.png new file mode 100644 index 00000000..eda6250d Binary files /dev/null and b/assets/images/3.0x/img_star_rate_disable.png differ diff --git a/assets/images/3.0x/img_touch.png b/assets/images/3.0x/img_touch.png new file mode 100644 index 00000000..38812507 Binary files /dev/null and b/assets/images/3.0x/img_touch.png differ diff --git a/assets/images/4.0x/img_app_icon.png b/assets/images/4.0x/img_app_icon.png new file mode 100644 index 00000000..2c3a3a87 Binary files /dev/null and b/assets/images/4.0x/img_app_icon.png differ diff --git a/assets/images/4.0x/img_clef.png b/assets/images/4.0x/img_clef.png new file mode 100644 index 00000000..31107b6b Binary files /dev/null and b/assets/images/4.0x/img_clef.png differ diff --git a/assets/images/4.0x/img_clock.png b/assets/images/4.0x/img_clock.png new file mode 100644 index 00000000..505f963e Binary files /dev/null and b/assets/images/4.0x/img_clock.png differ diff --git a/assets/images/4.0x/img_facebook.png b/assets/images/4.0x/img_facebook.png new file mode 100644 index 00000000..0692ea1a Binary files /dev/null and b/assets/images/4.0x/img_facebook.png differ diff --git a/assets/images/4.0x/img_google.png b/assets/images/4.0x/img_google.png new file mode 100644 index 00000000..50c23f6a Binary files /dev/null and b/assets/images/4.0x/img_google.png differ diff --git a/assets/images/4.0x/img_guitar.png b/assets/images/4.0x/img_guitar.png new file mode 100644 index 00000000..72720bf5 Binary files /dev/null and b/assets/images/4.0x/img_guitar.png differ diff --git a/assets/images/4.0x/img_note.png b/assets/images/4.0x/img_note.png new file mode 100644 index 00000000..2405b93f Binary files /dev/null and b/assets/images/4.0x/img_note.png differ diff --git a/assets/images/4.0x/img_single_note.png b/assets/images/4.0x/img_single_note.png new file mode 100644 index 00000000..0379af97 Binary files /dev/null and b/assets/images/4.0x/img_single_note.png differ diff --git a/assets/images/4.0x/img_staff.png b/assets/images/4.0x/img_staff.png new file mode 100644 index 00000000..03c18512 Binary files /dev/null and b/assets/images/4.0x/img_staff.png differ diff --git a/assets/images/4.0x/img_star.png b/assets/images/4.0x/img_star.png new file mode 100644 index 00000000..389d6c6a Binary files /dev/null and b/assets/images/4.0x/img_star.png differ diff --git a/assets/images/4.0x/img_star_rate.png b/assets/images/4.0x/img_star_rate.png new file mode 100644 index 00000000..a29d5985 Binary files /dev/null and b/assets/images/4.0x/img_star_rate.png differ diff --git a/assets/images/4.0x/img_star_rate_disable.png b/assets/images/4.0x/img_star_rate_disable.png new file mode 100644 index 00000000..4f9809fd Binary files /dev/null and b/assets/images/4.0x/img_star_rate_disable.png differ diff --git a/assets/images/4.0x/img_touch.png b/assets/images/4.0x/img_touch.png new file mode 100644 index 00000000..438a0998 Binary files /dev/null and b/assets/images/4.0x/img_touch.png differ diff --git a/assets/images/img_app_icon.png b/assets/images/img_app_icon.png new file mode 100644 index 00000000..fdcb4e46 Binary files /dev/null and b/assets/images/img_app_icon.png differ diff --git a/assets/images/img_clef.png b/assets/images/img_clef.png new file mode 100644 index 00000000..be0db997 Binary files /dev/null and b/assets/images/img_clef.png differ diff --git a/assets/images/img_clock.png b/assets/images/img_clock.png new file mode 100644 index 00000000..d9e2ac4a Binary files /dev/null and b/assets/images/img_clock.png differ diff --git a/assets/images/img_facebook.png b/assets/images/img_facebook.png new file mode 100644 index 00000000..d4e80b3f Binary files /dev/null and b/assets/images/img_facebook.png differ diff --git a/assets/images/img_google.png b/assets/images/img_google.png new file mode 100644 index 00000000..7d6c5001 Binary files /dev/null and b/assets/images/img_google.png differ diff --git a/assets/images/img_guitar.png b/assets/images/img_guitar.png new file mode 100644 index 00000000..6bdb4dc9 Binary files /dev/null and b/assets/images/img_guitar.png differ diff --git a/assets/images/img_note.png b/assets/images/img_note.png new file mode 100644 index 00000000..3b307151 Binary files /dev/null and b/assets/images/img_note.png differ diff --git a/assets/images/img_single_note.png b/assets/images/img_single_note.png new file mode 100644 index 00000000..be5d19f8 Binary files /dev/null and b/assets/images/img_single_note.png differ diff --git a/assets/images/img_staff.png b/assets/images/img_staff.png new file mode 100644 index 00000000..1a96765d Binary files /dev/null and b/assets/images/img_staff.png differ diff --git a/assets/images/img_star.png b/assets/images/img_star.png new file mode 100644 index 00000000..e89131c6 Binary files /dev/null and b/assets/images/img_star.png differ diff --git a/assets/images/img_star_rate.png b/assets/images/img_star_rate.png new file mode 100644 index 00000000..96772cd8 Binary files /dev/null and b/assets/images/img_star_rate.png differ diff --git a/assets/images/img_star_rate_disable.png b/assets/images/img_star_rate_disable.png new file mode 100644 index 00000000..0f6ef4c7 Binary files /dev/null and b/assets/images/img_star_rate_disable.png differ diff --git a/assets/images/img_touch.png b/assets/images/img_touch.png new file mode 100644 index 00000000..881ec78f Binary files /dev/null and b/assets/images/img_touch.png differ diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 00000000..305336f1 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1,3 @@ +adminsdk-dev.json +adminsdk-stage.json +adminsdk-prod.json \ No newline at end of file diff --git a/database/backup-firestore.sh b/database/backup-firestore.sh new file mode 100644 index 00000000..6c616355 --- /dev/null +++ b/database/backup-firestore.sh @@ -0,0 +1,5 @@ +# curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - +# sudo apt-get install -y nodejs +# npm install -g node-firestore-import-export; +firestore-export --backupFile db.json --prettyPrint; +./jq-win64.exe --sort-keys . db.json > db1.json; rm db.json; mv db1.json db.json; \ No newline at end of file diff --git a/database/db.json b/database/db.json new file mode 100644 index 00000000..63564fb1 --- /dev/null +++ b/database/db.json @@ -0,0 +1,6072 @@ +{ + "__collections__": { + "instruments": { + "acoustic_guitar": { + "__collections__": {}, + "id": "acoustic_guitar", + "maxNote": 84, + "minNote": 31, + "soundFiles": { + "31": "sounds/acoustic-guitar/31.mp3", + "33": "sounds/acoustic-guitar/33.mp3", + "36": "sounds/acoustic-guitar/36.mp3", + "39": "sounds/acoustic-guitar/39.mp3", + "43": "sounds/acoustic-guitar/43.mp3", + "45": "sounds/acoustic-guitar/45.mp3", + "48": "sounds/acoustic-guitar/48.mp3", + "52": "sounds/acoustic-guitar/52.mp3", + "54": "sounds/acoustic-guitar/54.mp3", + "57": "sounds/acoustic-guitar/57.mp3", + "60": "sounds/acoustic-guitar/60.mp3", + "63": "sounds/acoustic-guitar/63.mp3", + "66": "sounds/acoustic-guitar/66.mp3", + "69": "sounds/acoustic-guitar/69.mp3", + "72": "sounds/acoustic-guitar/72.mp3", + "75": "sounds/acoustic-guitar/75.mp3", + "78": "sounds/acoustic-guitar/78.mp3", + "81": "sounds/acoustic-guitar/81.mp3", + "84": "sounds/acoustic-guitar/84.mp3" + }, + "soundNotes": { + "31": { + "note": 31, + "pitch": 1 + }, + "32": { + "note": 31, + "pitch": 1.0594631 + }, + "33": { + "note": 33, + "pitch": 1 + }, + "34": { + "note": 33, + "pitch": 1.0594631 + }, + "35": { + "note": 33, + "pitch": 1.122462 + }, + "36": { + "note": 36, + "pitch": 1 + }, + "37": { + "note": 36, + "pitch": 1.0594631 + }, + "38": { + "note": 36, + "pitch": 1.122462 + }, + "39": { + "note": 39, + "pitch": 1 + }, + "40": { + "note": 39, + "pitch": 1.0594631 + }, + "41": { + "note": 39, + "pitch": 1.122462 + }, + "42": { + "note": 43, + "pitch": 0.9438743 + }, + "43": { + "note": 43, + "pitch": 1 + }, + "44": { + "note": 43, + "pitch": 1.0594631 + }, + "45": { + "note": 45, + "pitch": 1 + }, + "46": { + "note": 45, + "pitch": 1.0594631 + }, + "47": { + "note": 45, + "pitch": 1.122462 + }, + "48": { + "note": 48, + "pitch": 1 + }, + "49": { + "note": 48, + "pitch": 1.0594631 + }, + "50": { + "note": 48, + "pitch": 1.122462 + }, + "51": { + "note": 52, + "pitch": 0.9438743 + }, + "52": { + "note": 52, + "pitch": 1 + }, + "53": { + "note": 52, + "pitch": 1.0594631 + }, + "54": { + "note": 54, + "pitch": 1 + }, + "55": { + "note": 54, + "pitch": 1.0594631 + }, + "56": { + "note": 54, + "pitch": 1.122462 + }, + "57": { + "note": 57, + "pitch": 1 + }, + "58": { + "note": 57, + "pitch": 1.0594631 + }, + "59": { + "note": 57, + "pitch": 1.122462 + }, + "60": { + "note": 60, + "pitch": 1 + }, + "61": { + "note": 60, + "pitch": 1.0594631 + }, + "62": { + "note": 60, + "pitch": 1.122462 + }, + "63": { + "note": 63, + "pitch": 1 + }, + "64": { + "note": 63, + "pitch": 1.0594631 + }, + "65": { + "note": 63, + "pitch": 1.122462 + }, + "66": { + "note": 66, + "pitch": 1 + }, + "67": { + "note": 66, + "pitch": 1.0594631 + }, + "68": { + "note": 66, + "pitch": 1.122462 + }, + "69": { + "note": 69, + "pitch": 1 + }, + "70": { + "note": 69, + "pitch": 1.0594631 + }, + "71": { + "note": 69, + "pitch": 1.122462 + }, + "72": { + "note": 72, + "pitch": 1 + }, + "73": { + "note": 72, + "pitch": 1.0594631 + }, + "74": { + "note": 72, + "pitch": 1.122462 + }, + "75": { + "note": 75, + "pitch": 1 + }, + "76": { + "note": 75, + "pitch": 1.0594631 + }, + "77": { + "note": 75, + "pitch": 1.122462 + }, + "78": { + "note": 78, + "pitch": 1 + }, + "79": { + "note": 78, + "pitch": 1.0594631 + }, + "80": { + "note": 78, + "pitch": 1.122462 + }, + "81": { + "note": 81, + "pitch": 1 + }, + "82": { + "note": 81, + "pitch": 1.0594631 + }, + "83": { + "note": 81, + "pitch": 1.122462 + }, + "84": { + "note": 84, + "pitch": 1 + } + }, + "volume": 0.5 + }, + "electric_guitar": { + "__collections__": {}, + "id": "electric_guitar", + "maxNote": 84, + "minNote": 40, + "soundFiles": { + "40": "sounds/electric-guitar/40.mp3", + "44": "sounds/electric-guitar/44.mp3", + "48": "sounds/electric-guitar/48.mp3", + "52": "sounds/electric-guitar/52.mp3", + "56": "sounds/electric-guitar/56.mp3", + "60": "sounds/electric-guitar/60.mp3", + "65": "sounds/electric-guitar/65.mp3", + "68": "sounds/electric-guitar/68.mp3", + "72": "sounds/electric-guitar/72.mp3", + "76": "sounds/electric-guitar/76.mp3", + "80": "sounds/electric-guitar/80.mp3", + "84": "sounds/electric-guitar/84.mp3" + }, + "soundNotes": { + "40": { + "note": 40, + "pitch": 1 + }, + "41": { + "note": 40, + "pitch": 1.0594631 + }, + "42": { + "note": 40, + "pitch": 1.122462 + }, + "43": { + "note": 44, + "pitch": 0.9438743 + }, + "44": { + "note": 44, + "pitch": 1 + }, + "45": { + "note": 44, + "pitch": 1.0594631 + }, + "46": { + "note": 44, + "pitch": 1.122462 + }, + "47": { + "note": 48, + "pitch": 0.9438743 + }, + "48": { + "note": 48, + "pitch": 1 + }, + "49": { + "note": 48, + "pitch": 1.0594631 + }, + "50": { + "note": 48, + "pitch": 1.122462 + }, + "51": { + "note": 52, + "pitch": 0.9438743 + }, + "52": { + "note": 52, + "pitch": 1 + }, + "53": { + "note": 52, + "pitch": 1.0594631 + }, + "54": { + "note": 52, + "pitch": 1.122462 + }, + "55": { + "note": 56, + "pitch": 0.9438743 + }, + "56": { + "note": 56, + "pitch": 1 + }, + "57": { + "note": 56, + "pitch": 1.0594631 + }, + "58": { + "note": 56, + "pitch": 1.122462 + }, + "59": { + "note": 60, + "pitch": 0.9438743 + }, + "60": { + "note": 60, + "pitch": 1 + }, + "61": { + "note": 60, + "pitch": 1.0594631 + }, + "62": { + "note": 60, + "pitch": 1.122462 + }, + "63": { + "note": 65, + "pitch": 0.8908987 + }, + "64": { + "note": 65, + "pitch": 0.9438743 + }, + "65": { + "note": 65, + "pitch": 1 + }, + "66": { + "note": 65, + "pitch": 1.0594631 + }, + "67": { + "note": 65, + "pitch": 1.122462 + }, + "68": { + "note": 68, + "pitch": 1 + }, + "69": { + "note": 68, + "pitch": 1.0594631 + }, + "70": { + "note": 68, + "pitch": 1.122462 + }, + "71": { + "note": 72, + "pitch": 0.9438743 + }, + "72": { + "note": 72, + "pitch": 1 + }, + "73": { + "note": 72, + "pitch": 1.0594631 + }, + "74": { + "note": 72, + "pitch": 1.122462 + }, + "75": { + "note": 76, + "pitch": 0.9438743 + }, + "76": { + "note": 76, + "pitch": 1 + }, + "77": { + "note": 76, + "pitch": 1.0594631 + }, + "78": { + "note": 76, + "pitch": 1.122462 + }, + "79": { + "note": 80, + "pitch": 0.9438743 + }, + "80": { + "note": 80, + "pitch": 1 + }, + "81": { + "note": 80, + "pitch": 1.0594631 + }, + "82": { + "note": 80, + "pitch": 1.122462 + }, + "83": { + "note": 84, + "pitch": 0.9438743 + }, + "84": { + "note": 84, + "pitch": 1 + } + }, + "volume": 0.3 + }, + "piano": { + "__collections__": {}, + "id": "piano", + "maxNote": 108, + "minNote": 24, + "soundFiles": { + "102": "sounds/piano/102.mp3", + "105": "sounds/piano/105.mp3", + "107": "sounds/piano/107.mp3", + "24": "sounds/piano/24.mp3", + "30": "sounds/piano/30.mp3", + "35": "sounds/piano/35.mp3", + "39": "sounds/piano/39.mp3", + "42": "sounds/piano/42.mp3", + "47": "sounds/piano/47.mp3", + "51": "sounds/piano/51.mp3", + "54": "sounds/piano/54.mp3", + "57": "sounds/piano/57.mp3", + "60": "sounds/piano/60.mp3", + "63": "sounds/piano/63.mp3", + "66": "sounds/piano/66.mp3", + "69": "sounds/piano/69.mp3", + "72": "sounds/piano/72.mp3", + "75": "sounds/piano/75.mp3", + "78": "sounds/piano/78.mp3", + "81": "sounds/piano/81.mp3", + "84": "sounds/piano/84.mp3", + "87": "sounds/piano/87.mp3", + "90": "sounds/piano/90.mp3", + "93": "sounds/piano/93.mp3", + "96": "sounds/piano/96.mp3", + "99": "sounds/piano/99.mp3" + }, + "soundNotes": { + "100": { + "note": 99, + "pitch": 1.0594631 + }, + "101": { + "note": 99, + "pitch": 1.122462 + }, + "102": { + "note": 102, + "pitch": 1 + }, + "103": { + "note": 102, + "pitch": 1.0594631 + }, + "104": { + "note": 102, + "pitch": 1.122462 + }, + "105": { + "note": 105, + "pitch": 1 + }, + "106": { + "note": 105, + "pitch": 1.0594631 + }, + "107": { + "note": 107, + "pitch": 1 + }, + "108": { + "note": 107, + "pitch": 1.0594631 + }, + "24": { + "note": 24, + "pitch": 1 + }, + "25": { + "note": 24, + "pitch": 1.0594631 + }, + "26": { + "note": 24, + "pitch": 1.122462 + }, + "27": { + "note": 30, + "pitch": 0.8408964 + }, + "28": { + "note": 30, + "pitch": 0.8908987 + }, + "29": { + "note": 30, + "pitch": 0.9438743 + }, + "30": { + "note": 30, + "pitch": 1 + }, + "31": { + "note": 30, + "pitch": 1.0594631 + }, + "32": { + "note": 30, + "pitch": 1.122462 + }, + "33": { + "note": 35, + "pitch": 0.8908987 + }, + "34": { + "note": 35, + "pitch": 0.9438743 + }, + "35": { + "note": 35, + "pitch": 1 + }, + "36": { + "note": 35, + "pitch": 1.0594631 + }, + "37": { + "note": 35, + "pitch": 1.122462 + }, + "38": { + "note": 39, + "pitch": 0.9438743 + }, + "39": { + "note": 39, + "pitch": 1 + }, + "40": { + "note": 39, + "pitch": 1.0594631 + }, + "41": { + "note": 39, + "pitch": 1.122462 + }, + "42": { + "note": 42, + "pitch": 1 + }, + "43": { + "note": 42, + "pitch": 1.0594631 + }, + "44": { + "note": 42, + "pitch": 1.122462 + }, + "45": { + "note": 47, + "pitch": 0.8908987 + }, + "46": { + "note": 47, + "pitch": 0.9438743 + }, + "47": { + "note": 47, + "pitch": 1 + }, + "48": { + "note": 47, + "pitch": 1.0594631 + }, + "49": { + "note": 47, + "pitch": 1.122462 + }, + "50": { + "note": 51, + "pitch": 0.9438743 + }, + "51": { + "note": 51, + "pitch": 1 + }, + "52": { + "note": 51, + "pitch": 1.0594631 + }, + "53": { + "note": 51, + "pitch": 1.122462 + }, + "54": { + "note": 54, + "pitch": 1 + }, + "55": { + "note": 54, + "pitch": 1.0594631 + }, + "56": { + "note": 54, + "pitch": 1.122462 + }, + "57": { + "note": 57, + "pitch": 1 + }, + "58": { + "note": 57, + "pitch": 1.0594631 + }, + "59": { + "note": 57, + "pitch": 1.122462 + }, + "60": { + "note": 60, + "pitch": 1 + }, + "61": { + "note": 60, + "pitch": 1.0594631 + }, + "62": { + "note": 60, + "pitch": 1.122462 + }, + "63": { + "note": 63, + "pitch": 1 + }, + "64": { + "note": 63, + "pitch": 1.0594631 + }, + "65": { + "note": 63, + "pitch": 1.122462 + }, + "66": { + "note": 66, + "pitch": 1 + }, + "67": { + "note": 66, + "pitch": 1.0594631 + }, + "68": { + "note": 66, + "pitch": 1.122462 + }, + "69": { + "note": 69, + "pitch": 1 + }, + "70": { + "note": 69, + "pitch": 1.0594631 + }, + "71": { + "note": 69, + "pitch": 1.122462 + }, + "72": { + "note": 72, + "pitch": 1 + }, + "73": { + "note": 72, + "pitch": 1.0594631 + }, + "74": { + "note": 72, + "pitch": 1.122462 + }, + "75": { + "note": 75, + "pitch": 1 + }, + "76": { + "note": 75, + "pitch": 1.0594631 + }, + "77": { + "note": 75, + "pitch": 1.122462 + }, + "78": { + "note": 78, + "pitch": 1 + }, + "79": { + "note": 78, + "pitch": 1.0594631 + }, + "80": { + "note": 78, + "pitch": 1.122462 + }, + "81": { + "note": 81, + "pitch": 1 + }, + "82": { + "note": 81, + "pitch": 1.0594631 + }, + "83": { + "note": 81, + "pitch": 1.122462 + }, + "84": { + "note": 84, + "pitch": 1 + }, + "85": { + "note": 84, + "pitch": 1.0594631 + }, + "86": { + "note": 84, + "pitch": 1.122462 + }, + "87": { + "note": 87, + "pitch": 1 + }, + "88": { + "note": 87, + "pitch": 1.0594631 + }, + "89": { + "note": 87, + "pitch": 1.122462 + }, + "90": { + "note": 90, + "pitch": 1 + }, + "91": { + "note": 90, + "pitch": 1.0594631 + }, + "92": { + "note": 90, + "pitch": 1.122462 + }, + "93": { + "note": 93, + "pitch": 1 + }, + "94": { + "note": 93, + "pitch": 1.0594631 + }, + "95": { + "note": 93, + "pitch": 1.122462 + }, + "96": { + "note": 96, + "pitch": 1 + }, + "97": { + "note": 96, + "pitch": 1.0594631 + }, + "98": { + "note": 96, + "pitch": 1.122462 + }, + "99": { + "note": 99, + "pitch": 1 + } + }, + "volume": 0.5 + } + }, + "songs": { + "Alan_Walker-Alone": { + "__collections__": {}, + "artist": "Alan Walker", + "bpm": 194, + "duration": [ + 363436426, + 272577320, + 218061856 + ], + "id": "Alan_Walker-Alone", + "tags": [ + "pop" + ], + "tilesCount": [ + 740, + 951, + 1067 + ], + "title": "Alone", + "url": "songs/pop/Alan_Walker-Alone.mid" + }, + "Alan_Walker-Faded": { + "__collections__": {}, + "artist": "Alan Walker", + "bpm": 89, + "duration": [ + 318202247, + 238651685, + 190921348 + ], + "id": "Alan_Walker-Faded", + "tags": [ + "pop" + ], + "tilesCount": [ + 829, + 865, + 882 + ], + "title": "Faded", + "url": "songs/pop/Alan_Walker-Faded.mid" + }, + "Alan_Walker-Spectre": { + "__collections__": {}, + "artist": "Alan Walker", + "bpm": 128, + "duration": [ + 299895833, + 224921875, + 179937500 + ], + "id": "Alan_Walker-Spectre", + "tags": [ + "pop" + ], + "tilesCount": [ + 1226, + 1255, + 1255 + ], + "title": "Spectre", + "url": "songs/pop/Alan_Walker-Spectre.mid" + }, + "Aram_Khachaturian-Masquerade_Waltz": { + "__collections__": {}, + "artist": "Aram Khachaturian", + "bpm": 150, + "duration": [ + 419466667, + 314600000, + 251680000 + ], + "id": "Aram_Khachaturian-Masquerade_Waltz", + "tags": [ + "classic" + ], + "tilesCount": [ + 1787, + 2310, + 2573 + ], + "title": "Masquerade Waltz", + "url": "songs/classic/Aram_Khachaturian-Masquerade_Waltz.mid" + }, + "Aram_Khachaturian-Sabr_Dance": { + "__collections__": {}, + "artist": "Aram Khachaturian", + "bpm": 150, + "duration": [ + 305392593, + 229044444, + 183235556 + ], + "id": "Aram_Khachaturian-Sabr_Dance", + "tags": [ + "classic" + ], + "tilesCount": [ + 1642, + 2110, + 2347 + ], + "title": "Sabr Dance", + "url": "songs/classic/Aram_Khachaturian-Sabr_Dance.mid" + }, + "BTS-Black_Swan": { + "__collections__": {}, + "artist": "BTS", + "bpm": 147, + "duration": [ + 290884354, + 218163265, + 174530612 + ], + "id": "BTS-Black_Swan", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1165, + 1226, + 1262 + ], + "title": "Black Swan", + "url": "songs/kpop/BTS-Black_Swan.mid" + }, + "BTS-DNA": { + "__collections__": {}, + "artist": "BTS", + "bpm": 130, + "duration": [ + 161025641, + 120769231, + 96615385 + ], + "id": "BTS-DNA", + "tags": [ + "kpop" + ], + "tilesCount": [ + 664, + 846, + 962 + ], + "title": "DNA", + "url": "songs/kpop/BTS-DNA.mid" + }, + "BTS-Dynamite": { + "__collections__": {}, + "artist": "BTS", + "bpm": 115, + "duration": [ + 470260870, + 352695652, + 282156522 + ], + "id": "BTS-Dynamite", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1397, + 1757, + 1929 + ], + "title": "Dynamite", + "url": "songs/kpop/BTS-Dynamite.mid" + }, + "BTS-I_Need_U": { + "__collections__": {}, + "artist": "BTS", + "bpm": 150, + "duration": [ + 325729630, + 244297222, + 195437778 + ], + "id": "BTS-I_Need_U", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1332, + 1586, + 1748 + ], + "title": "I Need U", + "url": "songs/kpop/BTS-I_Need_U.mid" + }, + "BTS-Life_Goes_On": { + "__collections__": {}, + "artist": "BTS", + "bpm": 74, + "duration": [ + 293963964, + 220472973, + 176378378 + ], + "id": "BTS-Life_Goes_On", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1125, + 1361, + 1432 + ], + "title": "Life Goes On", + "url": "songs/kpop/BTS-Life_Goes_On.mid" + }, + "Bach-Air_On_The_G_String": { + "__collections__": {}, + "artist": "Bach", + "bpm": 45, + "duration": [ + 276691358, + 207518519, + 166014815 + ], + "id": "Bach-Air_On_The_G_String", + "tags": [ + "classic" + ], + "tilesCount": [ + 650, + 669, + 674 + ], + "title": "Air On The G String", + "url": "songs/classic/Bach-Air_On_The_G_String.mid" + }, + "Bach-Badinerie": { + "__collections__": {}, + "artist": "Bach", + "bpm": 75, + "duration": [ + 184355556, + 138266667, + 110613333 + ], + "id": "Bach-Badinerie", + "tags": [ + "classic" + ], + "tilesCount": [ + 914, + 1160, + 1310 + ], + "title": "Badinerie", + "url": "songs/classic/Bach-Badinerie.mid" + }, + "Bach-Bourree_In_E_Minor": { + "__collections__": {}, + "artist": "Bach", + "bpm": 107, + "duration": [ + 150031153, + 112523364, + 90018692 + ], + "id": "Bach-Bourree_In_E_Minor", + "tags": [ + "classic" + ], + "tilesCount": [ + 472, + 474, + 476 + ], + "title": "Bourree In E Minor", + "url": "songs/classic/Bach-Bourree_In_E_Minor.mid" + }, + "Bach-Cello_Suite": { + "__collections__": {}, + "artist": "Bach", + "bpm": 69, + "duration": [ + 190628019, + 142971014, + 114376812 + ], + "id": "Bach-Cello_Suite", + "tags": [ + "classic" + ], + "tilesCount": [ + 1096, + 1111, + 1126 + ], + "title": "Cello Suite", + "url": "songs/classic/Bach-Cello_Suite.mid" + }, + "Bach-Joy": { + "__collections__": {}, + "artist": "Bach", + "bpm": 64, + "duration": [ + 276157407, + 207118056, + 165694444 + ], + "id": "Bach-Joy", + "tags": [ + "classic" + ], + "tilesCount": [ + 866, + 993, + 1018 + ], + "title": "Joy", + "url": "songs/classic/Bach-Joy.mid" + }, + "Bach-Minuet_In_G": { + "__collections__": {}, + "artist": "Bach", + "bpm": 120, + "duration": [ + 148888889, + 111666667, + 89333333 + ], + "id": "Bach-Minuet_In_G", + "tags": [ + "classic" + ], + "tilesCount": [ + 422, + 428, + 432 + ], + "title": "Minuet In G", + "url": "songs/classic/Bach-Minuet_In_G.mid" + }, + "Bach-Toccata_and_Fugue": { + "__collections__": {}, + "artist": "Bach", + "bpm": 60, + "duration": [ + 953722222, + 715291667, + 572233333 + ], + "id": "Bach-Toccata_and_Fugue", + "tags": [ + "classic" + ], + "tilesCount": [ + 3500, + 3794, + 3942 + ], + "title": "Toccata and Fugue", + "url": "songs/classic/Bach-Toccata_and_Fugue.mid" + }, + "Bach-Well_Tempered_Clavier": { + "__collections__": {}, + "artist": "Bach", + "bpm": 72, + "duration": [ + 147129630, + 110347222, + 88277778 + ], + "id": "Bach-Well_Tempered_Clavier", + "tags": [ + "classic" + ], + "tilesCount": [ + 530, + 531, + 532 + ], + "title": "Well Tempered Clavier", + "url": "songs/classic/Bach-Well_Tempered_Clavier.mid" + }, + "Beethoven-Appasionata_Sonate": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 72, + "duration": [ + 423148148, + 317361111, + 253888889 + ], + "id": "Beethoven-Appasionata_Sonate", + "tags": [ + "classic" + ], + "tilesCount": [ + 774, + 929, + 989 + ], + "title": "Appasionata Sonate", + "url": "songs/classic/Beethoven-Appasionata_Sonate.mid" + }, + "Beethoven-Fur_Elise": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 53, + "duration": [ + 315471698, + 236603774, + 189283019 + ], + "id": "Beethoven-Fur_Elise", + "tags": [ + "classic" + ], + "tilesCount": [ + 940, + 999, + 1035 + ], + "title": "Fur Elise", + "url": "songs/classic/Beethoven-Fur_Elise.mid" + }, + "Beethoven-Moonlight_Sonata_Mvt._1": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 48, + "duration": [ + 474320988, + 355740741, + 284592593 + ], + "id": "Beethoven-Moonlight_Sonata_Mvt._1", + "tags": [ + "classic" + ], + "tilesCount": [ + 959, + 1065, + 1132 + ], + "title": "Moonlight Sonata Mvt. 1", + "url": "songs/classic/Beethoven-Moonlight_Sonata_Mvt._1.mid" + }, + "Beethoven-Moonlight_Sonata_Mvt._3": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 75, + "duration": [ + 1189562963, + 892172222, + 713737778 + ], + "id": "Beethoven-Moonlight_Sonata_Mvt._3", + "tags": [ + "classic" + ], + "tilesCount": [ + 5245, + 5912, + 6346 + ], + "title": "Moonlight Sonata Mvt. 3", + "url": "songs/classic/Beethoven-Moonlight_Sonata_Mvt._3.mid" + }, + "Beethoven-String_Quartet": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 75, + "duration": [ + 969037037, + 726777778, + 581422222 + ], + "id": "Beethoven-String_Quartet", + "tags": [ + "classic" + ], + "tilesCount": [ + 3011, + 3466, + 3803 + ], + "title": "String Quartet", + "url": "songs/classic/Beethoven-String_Quartet.mid" + }, + "Beethoven-Symphony_No._3_Mvt._1": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 150, + "duration": [ + 397037037, + 297777778, + 238222222 + ], + "id": "Beethoven-Symphony_No._3_Mvt._1", + "tags": [ + "classic" + ], + "tilesCount": [ + 1542, + 1992, + 2264 + ], + "title": "Symphony No. 3 Mvt. 1", + "url": "songs/classic/Beethoven-Symphony_No._3_Mvt._1.mid" + }, + "Beethoven-Symphony_No._5_Mvt._1": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 149, + "duration": [ + 159731544, + 119798658, + 95838926 + ], + "id": "Beethoven-Symphony_No._5_Mvt._1", + "tags": [ + "classic" + ], + "tilesCount": [ + 695, + 889, + 1001 + ], + "title": "Symphony No. 5 Mvt. 1", + "url": "songs/classic/Beethoven-Symphony_No._5_Mvt._1.mid" + }, + "Beethoven-Symphony_No._5_Mvt._3": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 149, + "duration": [ + 633944817, + 475458613, + 380366890 + ], + "id": "Beethoven-Symphony_No._5_Mvt._3", + "tags": [ + "classic" + ], + "tilesCount": [ + 2059, + 2559, + 2932 + ], + "title": "Symphony No. 5 Mvt. 3", + "url": "songs/classic/Beethoven-Symphony_No._5_Mvt._3.mid" + }, + "Beethoven-Symphony_No._6": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 75, + "duration": [ + 772051852, + 579038889, + 463231111 + ], + "id": "Beethoven-Symphony_No._6", + "tags": [ + "classic" + ], + "tilesCount": [ + 3426, + 4215, + 4675 + ], + "title": "Symphony No. 6", + "url": "songs/classic/Beethoven-Symphony_No._6.mid" + }, + "Beethoven-Symphony_No._7_Mvt._2": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 72, + "duration": [ + 668888889, + 501666667, + 401333333 + ], + "id": "Beethoven-Symphony_No._7_Mvt._2", + "tags": [ + "classic" + ], + "tilesCount": [ + 2468, + 3019, + 3361 + ], + "title": "Symphony No. 7 Mvt. 2", + "url": "songs/classic/Beethoven-Symphony_No._7_Mvt._2.mid" + }, + "Beethoven-Symphony_No._9_Mvt._2": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 299, + "duration": [ + 2230635452, + 1672976589, + 1338381271 + ], + "id": "Beethoven-Symphony_No._9_Mvt._2", + "tags": [ + "classic" + ], + "tilesCount": [ + 8557, + 11600, + 13686 + ], + "title": "Symphony No. 9 Mvt. 2", + "url": "songs/classic/Beethoven-Symphony_No._9_Mvt._2.mid" + }, + "Beethoven-Symphony_No._9_Mvt._4": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 123, + "duration": [ + 169539295, + 127154472, + 101723577 + ], + "id": "Beethoven-Symphony_No._9_Mvt._4", + "tags": [ + "classic" + ], + "tilesCount": [ + 208, + 266, + 266 + ], + "title": "Symphony No. 9 Mvt. 4", + "url": "songs/classic/Beethoven-Symphony_No._9_Mvt._4.mid" + }, + "Beethoven-Turkish_March": { + "__collections__": {}, + "artist": "Beethoven", + "bpm": 120, + "duration": [ + 375166667, + 281375000, + 225100000 + ], + "id": "Beethoven-Turkish_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 1178, + 1613, + 1916 + ], + "title": "Turkish March", + "url": "songs/classic/Beethoven-Turkish_March.mid" + }, + "Billie_Eilish,_Khalid-Lovely": { + "__collections__": {}, + "artist": "Billie Eilish, Khalid", + "bpm": 115, + "duration": [ + 278067633, + 208550725, + 166840580 + ], + "id": "Billie_Eilish,_Khalid-Lovely", + "tags": [ + "other" + ], + "tilesCount": [ + 542, + 629, + 713 + ], + "title": "Lovely", + "url": "songs/other/Billie_Eilish,_Khalid-Lovely.mid" + }, + "Blackpink-How_You_Like_That": { + "__collections__": {}, + "artist": "Blackpink", + "bpm": 130, + "duration": [ + 325435897, + 244076923, + 195261538 + ], + "id": "Blackpink-How_You_Like_That", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1146, + 1369, + 1445 + ], + "title": "How You Like That", + "url": "songs/kpop/Blackpink-How_You_Like_That.mid" + }, + "Blackpink-Kill_This_Love_Piano": { + "__collections__": {}, + "artist": "Blackpink", + "bpm": 100, + "duration": [ + 342992593, + 257244444, + 205795556 + ], + "id": "Blackpink-Kill_This_Love_Piano", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1217, + 1452, + 1485 + ], + "title": "Kill This Love Piano", + "url": "songs/kpop/Blackpink-Kill_This_Love_Piano.mid" + }, + "Blackpink-You_Never_Know": { + "__collections__": {}, + "artist": "Blackpink", + "bpm": 75, + "duration": [ + 292000000, + 219000000, + 175200000 + ], + "id": "Blackpink-You_Never_Know", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1149, + 1320, + 1371 + ], + "title": "You Never Know", + "url": "songs/kpop/Blackpink-You_Never_Know.mid" + }, + "Brad_Breeck-Gravity_Falls,_Opening_Theme": { + "__collections__": {}, + "artist": "Brad Breeck", + "bpm": 150, + "duration": [ + 57422222, + 43066667, + 34453333 + ], + "id": "Brad_Breeck-Gravity_Falls,_Opening_Theme", + "tags": [ + "other" + ], + "tilesCount": [ + 271, + 332, + 338 + ], + "title": "Gravity Falls, Opening Theme", + "url": "songs/other/Brad_Breeck-Gravity_Falls,_Opening_Theme.mid" + }, + "Bruno_Mars-When_I_Was_Your_Man": { + "__collections__": {}, + "artist": "Bruno Mars", + "bpm": 73, + "duration": [ + 299178082, + 224383562, + 179506849 + ], + "id": "Bruno_Mars-When_I_Was_Your_Man", + "tags": [ + "pop" + ], + "tilesCount": [ + 598, + 831, + 940 + ], + "title": "When I Was Your Man", + "url": "songs/pop/Bruno_Mars-When_I_Was_Your_Man.mid" + }, + "Carl_Orff-O_Fortuna": { + "__collections__": {}, + "artist": "Carl Orff", + "bpm": 120, + "duration": [ + 209111111, + 156833333, + 125466667 + ], + "id": "Carl_Orff-O_Fortuna", + "tags": [ + "classic" + ], + "tilesCount": [ + 835, + 939, + 958 + ], + "title": "O Fortuna", + "url": "songs/classic/Carl_Orff-O_Fortuna.mid" + }, + "Celine_Dion-My_Heart_Will_Go_On": { + "__collections__": {}, + "artist": "Celine Dion", + "bpm": 89, + "duration": [ + 444093633, + 333070225, + 266456180 + ], + "id": "Celine_Dion-My_Heart_Will_Go_On", + "tags": [ + "pop" + ], + "tilesCount": [ + 1188, + 1358, + 1402 + ], + "title": "My Heart Will Go On", + "url": "songs/pop/Celine_Dion-My_Heart_Will_Go_On.mid" + }, + "Charlie_Puth-See_You_Again": { + "__collections__": {}, + "artist": "Charlie Puth", + "bpm": 74, + "duration": [ + 380630631, + 285472973, + 228378378 + ], + "id": "Charlie_Puth-See_You_Again", + "tags": [ + "pop" + ], + "tilesCount": [ + 1215, + 1481, + 1647 + ], + "title": "See You Again", + "url": "songs/pop/Charlie_Puth-See_You_Again.mid" + }, + "Chinese_Folk-Jasmine_Flower": { + "__collections__": {}, + "artist": "Chinese Folk", + "bpm": 75, + "duration": [ + 58044444, + 43533333, + 34826667 + ], + "id": "Chinese_Folk-Jasmine_Flower", + "tags": [ + "folk" + ], + "tilesCount": [ + 284, + 284, + 284 + ], + "title": "Jasmine Flower", + "url": "songs/folk/Chinese_Folk-Jasmine_Flower.mid" + }, + "Chopin-Etude_Op._10_No._3": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 34, + "duration": [ + 380392157, + 285294118, + 228235294 + ], + "id": "Chopin-Etude_Op._10_No._3", + "tags": [ + "classic" + ], + "tilesCount": [ + 1212, + 1586, + 1777 + ], + "title": "Etude Op. 10 No. 3", + "url": "songs/classic/Chopin-Etude_Op._10_No._3.mid" + }, + "Chopin-Fantaisie_Impromptu": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 75, + "duration": [ + 840700000, + 630525000, + 504420000 + ], + "id": "Chopin-Fantaisie_Impromptu", + "tags": [ + "classic" + ], + "tilesCount": [ + 3027, + 3063, + 3067 + ], + "title": "Fantaisie Impromptu", + "url": "songs/classic/Chopin-Fantaisie_Impromptu.mid" + }, + "Chopin-Funeral_March": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 50, + "duration": [ + 440000000, + 330000000, + 264000000 + ], + "id": "Chopin-Funeral_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 504, + 576, + 591 + ], + "title": "Funeral March", + "url": "songs/classic/Chopin-Funeral_March.mid" + }, + "Chopin-Heroic_Polonaise": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 75, + "duration": [ + 596414815, + 447311111, + 357848889 + ], + "id": "Chopin-Heroic_Polonaise", + "tags": [ + "classic" + ], + "tilesCount": [ + 2723, + 3265, + 3471 + ], + "title": "Heroic Polonaise", + "url": "songs/classic/Chopin-Heroic_Polonaise.mid" + }, + "Chopin-Minute_Waltz": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 150, + "duration": [ + 269155556, + 201866667, + 161493333 + ], + "id": "Chopin-Minute_Waltz", + "tags": [ + "classic" + ], + "tilesCount": [ + 1121, + 1323, + 1423 + ], + "title": "Minute Waltz", + "url": "songs/classic/Chopin-Minute_Waltz.mid" + }, + "Chopin-Nocturne_Op._9_No._2": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 60, + "duration": [ + 734888889, + 551166667, + 440933333 + ], + "id": "Chopin-Nocturne_Op._9_No._2", + "tags": [ + "classic" + ], + "tilesCount": [ + 991, + 1199, + 1286 + ], + "title": "Nocturne Op. 9 No. 2", + "url": "songs/classic/Chopin-Nocturne_Op._9_No._2.mid" + }, + "Chopin-Revolutionary_Etude": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 75, + "duration": [ + 365644444, + 274233333, + 219386667 + ], + "id": "Chopin-Revolutionary_Etude", + "tags": [ + "classic" + ], + "tilesCount": [ + 1696, + 1881, + 2002 + ], + "title": "Revolutionary Etude", + "url": "songs/classic/Chopin-Revolutionary_Etude.mid" + }, + "Chopin-Waltz_Op._64_No._2": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 120, + "duration": [ + 429444444, + 322083333, + 257666667 + ], + "id": "Chopin-Waltz_Op._64_No._2", + "tags": [ + "classic" + ], + "tilesCount": [ + 1488, + 1769, + 1856 + ], + "title": "Waltz Op. 64 No. 2", + "url": "songs/classic/Chopin-Waltz_Op._64_No._2.mid" + }, + "Chopin-Waltz_in_A_Minor": { + "__collections__": {}, + "artist": "Chopin", + "bpm": 120, + "duration": [ + 234888889, + 176166667, + 140933333 + ], + "id": "Chopin-Waltz_in_A_Minor", + "tags": [ + "classic" + ], + "tilesCount": [ + 669, + 816, + 937 + ], + "title": "Waltz in A Minor", + "url": "songs/classic/Chopin-Waltz_in_A_Minor.mid" + }, + "Christina_Perri-A_Thousand_Years": { + "__collections__": {}, + "artist": "Christina Perri", + "bpm": 75, + "duration": [ + 2547200000, + 1910400000, + 1528320000 + ], + "id": "Christina_Perri-A_Thousand_Years", + "tags": [ + "pop" + ], + "tilesCount": [ + 560, + 764, + 912 + ], + "title": "A Thousand Years", + "url": "songs/pop/Christina_Perri-A_Thousand_Years.mid" + }, + "Deaf_Kev-Invincible": { + "__collections__": {}, + "artist": "Deaf Kev", + "bpm": 100, + "duration": [ + 862711111, + 647033333, + 517626667 + ], + "id": "Deaf_Kev-Invincible", + "tags": [ + "pop" + ], + "tilesCount": [ + 1766, + 2217, + 2579 + ], + "title": "Invincible", + "url": "songs/pop/Deaf_Kev-Invincible.mid" + }, + "Debussy-Clair_De_Lune": { + "__collections__": {}, + "artist": "Debussy", + "bpm": 54, + "duration": [ + 486181070, + 364635802, + 291708642 + ], + "id": "Debussy-Clair_De_Lune", + "tags": [ + "classic" + ], + "tilesCount": [ + 1092, + 1263, + 1352 + ], + "title": "Clair De Lune", + "url": "songs/classic/Debussy-Clair_De_Lune.mid" + }, + "Delibes-Flower_Duet": { + "__collections__": {}, + "artist": "Delibes", + "bpm": 75, + "duration": [ + 163748148, + 122811111, + 98248889 + ], + "id": "Delibes-Flower_Duet", + "tags": [ + "classic" + ], + "tilesCount": [ + 756, + 888, + 888 + ], + "title": "Flower Duet", + "url": "songs/classic/Delibes-Flower_Duet.mid" + }, + "Delibes-Pizzicato": { + "__collections__": {}, + "artist": "Delibes", + "bpm": 75, + "duration": [ + 170437037, + 127827778, + 102262222 + ], + "id": "Delibes-Pizzicato", + "tags": [ + "classic" + ], + "tilesCount": [ + 708, + 774, + 821 + ], + "title": "Pizzicato", + "url": "songs/classic/Delibes-Pizzicato.mid" + }, + "Different_Heaven,_EHDE-My_Heart": { + "__collections__": {}, + "artist": "Different Heaven, EHDE", + "bpm": 150, + "duration": [ + 431111111, + 323333333, + 258666667 + ], + "id": "Different_Heaven,_EHDE-My_Heart", + "tags": [ + "pop" + ], + "tilesCount": [ + 1943, + 2223, + 2373 + ], + "title": "My Heart", + "url": "songs/pop/Different_Heaven,_EHDE-My_Heart.mid" + }, + "Disfigure-Blank": { + "__collections__": {}, + "artist": "Disfigure", + "bpm": 130, + "duration": [ + 237333333, + 178000000, + 142400000 + ], + "id": "Disfigure-Blank", + "tags": [ + "pop" + ], + "tilesCount": [ + 1228, + 1476, + 1634 + ], + "title": "Blank", + "url": "songs/pop/Disfigure-Blank.mid" + }, + "Disney_Pixar's_Up-Married_Life": { + "__collections__": {}, + "artist": "Disney Pixar's Up", + "bpm": 150, + "duration": [ + 210462963, + 157847222, + 126277778 + ], + "id": "Disney_Pixar's_Up-Married_Life", + "tags": [ + "pop" + ], + "tilesCount": [ + 661, + 762, + 800 + ], + "title": "Married Life", + "url": "songs/pop/Disney_Pixar's_Up-Married_Life.mid" + }, + "Dmitri_Shostakovich-Second_Waltz": { + "__collections__": {}, + "artist": "Dmitri Shostakovich", + "bpm": 170, + "duration": [ + 530509804, + 397882353, + 318305882 + ], + "id": "Dmitri_Shostakovich-Second_Waltz", + "tags": [ + "classic" + ], + "tilesCount": [ + 1415, + 1936, + 2245 + ], + "title": "Second Waltz", + "url": "songs/classic/Dmitri_Shostakovich-Second_Waltz.mid" + }, + "Dvorak-Humoresque_No._7": { + "__collections__": {}, + "artist": "Dvorak", + "bpm": 72, + "duration": [ + 181450617, + 136087963, + 108870370 + ], + "id": "Dvorak-Humoresque_No._7", + "tags": [ + "classic" + ], + "tilesCount": [ + 751, + 930, + 998 + ], + "title": "Humoresque No. 7", + "url": "songs/classic/Dvorak-Humoresque_No._7.mid" + }, + "Dvorak-Slavonic_Dance_No._2": { + "__collections__": {}, + "artist": "Dvorak", + "bpm": 50, + "duration": [ + 480044444, + 360033333, + 288026667 + ], + "id": "Dvorak-Slavonic_Dance_No._2", + "tags": [ + "classic" + ], + "tilesCount": [ + 1646, + 2164, + 2521 + ], + "title": "Slavonic Dance No. 2", + "url": "songs/classic/Dvorak-Slavonic_Dance_No._2.mid" + }, + "Dvorak-Symphony_No._9_Mvt._4": { + "__collections__": {}, + "artist": "Dvorak", + "bpm": 100, + "duration": [ + 456177778, + 342133333, + 273706667 + ], + "id": "Dvorak-Symphony_No._9_Mvt._4", + "tags": [ + "classic" + ], + "tilesCount": [ + 1389, + 1726, + 1914 + ], + "title": "Symphony No. 9 Mvt. 4", + "url": "songs/classic/Dvorak-Symphony_No._9_Mvt._4.mid" + }, + "Ed_Sheeran-I_See_Fire": { + "__collections__": {}, + "artist": "Ed Sheeran", + "bpm": 160, + "duration": [ + 781833333, + 586375000, + 469100000 + ], + "id": "Ed_Sheeran-I_See_Fire", + "tags": [ + "pop" + ], + "tilesCount": [ + 1026, + 1242, + 1353 + ], + "title": "I See Fire", + "url": "songs/pop/Ed_Sheeran-I_See_Fire.mid" + }, + "Ed_Sheeran-Perfect": { + "__collections__": {}, + "artist": "Ed Sheeran", + "bpm": 94, + "duration": [ + 285957447, + 214468085, + 171574468 + ], + "id": "Ed_Sheeran-Perfect", + "tags": [ + "pop" + ], + "tilesCount": [ + 808, + 961, + 1018 + ], + "title": "Perfect", + "url": "songs/pop/Ed_Sheeran-Perfect.mid" + }, + "Ed_Sheeran-Photograph": { + "__collections__": {}, + "artist": "Ed Sheeran", + "bpm": 120, + "duration": [ + 378521296, + 283890972, + 227112778 + ], + "id": "Ed_Sheeran-Photograph", + "tags": [ + "pop" + ], + "tilesCount": [ + 1261, + 1472, + 1552 + ], + "title": "Photograph", + "url": "songs/pop/Ed_Sheeran-Photograph.mid" + }, + "Ed_Sheeran-Shape_Of_You": { + "__collections__": {}, + "artist": "Ed Sheeran", + "bpm": 149, + "duration": [ + 400357942, + 300268456, + 240214765 + ], + "id": "Ed_Sheeran-Shape_Of_You", + "tags": [ + "pop" + ], + "tilesCount": [ + 1527, + 1885, + 1922 + ], + "title": "Shape Of You", + "url": "songs/pop/Ed_Sheeran-Shape_Of_You.mid" + }, + "Ed_Sheeran-Thinking_Out_Loud": { + "__collections__": {}, + "artist": "Ed Sheeran", + "bpm": 74, + "duration": [ + 397199700, + 297899775, + 238319820 + ], + "id": "Ed_Sheeran-Thinking_Out_Loud", + "tags": [ + "pop" + ], + "tilesCount": [ + 916, + 1072, + 1113 + ], + "title": "Thinking Out Loud", + "url": "songs/pop/Ed_Sheeran-Thinking_Out_Loud.mid" + }, + "Edvard_Grieg-Anitras_Dance": { + "__collections__": {}, + "artist": "Edvard Grieg", + "bpm": 72, + "duration": [ + 724938272, + 543703704, + 434962963 + ], + "id": "Edvard_Grieg-Anitras_Dance", + "tags": [ + "classic" + ], + "tilesCount": [ + 1455, + 1741, + 1904 + ], + "title": "Anitras Dance", + "url": "songs/classic/Edvard_Grieg-Anitras_Dance.mid" + }, + "Edvard_Grieg-Morning_Mood": { + "__collections__": {}, + "artist": "Edvard Grieg", + "bpm": 75, + "duration": [ + 114874074, + 86155556, + 68924444 + ], + "id": "Edvard_Grieg-Morning_Mood", + "tags": [ + "classic" + ], + "tilesCount": [ + 440, + 536, + 571 + ], + "title": "Morning Mood", + "url": "songs/classic/Edvard_Grieg-Morning_Mood.mid" + }, + "Edvard_Grieg-Piano_Concerto": { + "__collections__": {}, + "artist": "Edvard Grieg", + "bpm": 37, + "duration": [ + 637717718, + 478288288, + 382630631 + ], + "id": "Edvard_Grieg-Piano_Concerto", + "tags": [ + "classic" + ], + "tilesCount": [ + 1818, + 2170, + 2411 + ], + "title": "Piano Concerto", + "url": "songs/classic/Edvard_Grieg-Piano_Concerto.mid" + }, + "Edward_Elgar-Pomp_And_Circumstance_March": { + "__collections__": {}, + "artist": "Edward Elgar", + "bpm": 75, + "duration": [ + 686281481, + 514711111, + 411768889 + ], + "id": "Edward_Elgar-Pomp_And_Circumstance_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 2299, + 2845, + 3168 + ], + "title": "Pomp And Circumstance March", + "url": "songs/classic/Edward_Elgar-Pomp_And_Circumstance_March.mid" + }, + "Elektronomia-Sky_High": { + "__collections__": {}, + "artist": "Elektronomia", + "bpm": 75, + "duration": [ + 332711111, + 249533333, + 199626667 + ], + "id": "Elektronomia-Sky_High", + "tags": [ + "pop" + ], + "tilesCount": [ + 1183, + 1362, + 1474 + ], + "title": "Sky High", + "url": "songs/pop/Elektronomia-Sky_High.mid" + }, + "English_Folk-Greensleeves": { + "__collections__": {}, + "artist": "English Folk", + "bpm": 72, + "duration": [ + 146296296, + 109722222, + 87777778 + ], + "id": "English_Folk-Greensleeves", + "tags": [ + "folk" + ], + "tilesCount": [ + 304, + 320, + 320 + ], + "title": "Greensleeves", + "url": "songs/folk/English_Folk-Greensleeves.mid" + }, + "English_Folk-Twinkle_Twinkle_Little_Star": { + "__collections__": {}, + "artist": "English Folk", + "bpm": 120, + "duration": [ + 134666667, + 101000000, + 80800000 + ], + "id": "English_Folk-Twinkle_Twinkle_Little_Star", + "tags": [ + "folk" + ], + "tilesCount": [ + 423, + 441, + 458 + ], + "title": "Twinkle Twinkle Little Star", + "url": "songs/folk/English_Folk-Twinkle_Twinkle_Little_Star.mid" + }, + "English_Folk-We_Wish_You_A_Marry_Christmas": { + "__collections__": {}, + "artist": "English Folk", + "bpm": 120, + "duration": [ + 118444444, + 88833333, + 71066667 + ], + "id": "English_Folk-We_Wish_You_A_Marry_Christmas", + "tags": [ + "folk" + ], + "tilesCount": [ + 166, + 246, + 313 + ], + "title": "We Wish You A Marry Christmas", + "url": "songs/folk/English_Folk-We_Wish_You_A_Marry_Christmas.mid" + }, + "English_Folk-We_Wish_You_A_Merry_Christmas": { + "__collections__": {}, + "artist": "English Folk", + "bpm": 150, + "duration": [ + 99422222, + 74566667, + 59653333 + ], + "id": "English_Folk-We_Wish_You_A_Merry_Christmas", + "tags": [ + "folk" + ], + "tilesCount": [ + 443, + 552, + 600 + ], + "title": "We Wish You A Merry Christmas", + "url": "songs/folk/English_Folk-We_Wish_You_A_Merry_Christmas.mid" + }, + "Erik_Satie-Gnossienne": { + "__collections__": {}, + "artist": "Erik Satie", + "bpm": 102, + "duration": [ + 904572985, + 678429739, + 542743791 + ], + "id": "Erik_Satie-Gnossienne", + "tags": [ + "other" + ], + "tilesCount": [ + 627, + 787, + 815 + ], + "title": "Gnossienne", + "url": "songs/other/Erik_Satie-Gnossienne.mid" + }, + "Erik_Satie-Gymnopedie": { + "__collections__": {}, + "artist": "Erik Satie", + "bpm": 54, + "duration": [ + 352098765, + 264074074, + 211259259 + ], + "id": "Erik_Satie-Gymnopedie", + "tags": [ + "other" + ], + "tilesCount": [ + 316, + 394, + 437 + ], + "title": "Gymnopedie", + "url": "songs/other/Erik_Satie-Gymnopedie.mid" + }, + "Eugen_Doga-My_Sweet_And_Tender_Beast": { + "__collections__": {}, + "artist": "Eugen Doga", + "bpm": 120, + "duration": [ + 171555556, + 128666667, + 102933333 + ], + "id": "Eugen_Doga-My_Sweet_And_Tender_Beast", + "tags": [ + "classic" + ], + "tilesCount": [ + 728, + 831, + 901 + ], + "title": "My Sweet And Tender Beast", + "url": "songs/classic/Eugen_Doga-My_Sweet_And_Tender_Beast.mid" + }, + "Eva_Cassidy-Autumn_Leaves": { + "__collections__": {}, + "artist": "Eva Cassidy", + "bpm": 120, + "duration": [ + 284888889, + 213666667, + 170933333 + ], + "id": "Eva_Cassidy-Autumn_Leaves", + "tags": [ + "other" + ], + "tilesCount": [ + 813, + 858, + 861 + ], + "title": "Autumn Leaves", + "url": "songs/other/Eva_Cassidy-Autumn_Leaves.mid" + }, + "Frank_Sinatra-Fly_Me_To_The_Moon": { + "__collections__": {}, + "artist": "Frank Sinatra", + "bpm": 139, + "duration": [ + 699721823, + 524791367, + 419833094 + ], + "id": "Frank_Sinatra-Fly_Me_To_The_Moon", + "tags": [ + "other" + ], + "tilesCount": [ + 581, + 668, + 694 + ], + "title": "Fly Me To The Moon", + "url": "songs/other/Frank_Sinatra-Fly_Me_To_The_Moon.mid" + }, + "Franz_Liszt-Hungarian_Rhapsody_No._2": { + "__collections__": {}, + "artist": "Franz Liszt", + "bpm": 240, + "duration": [ + 313333333, + 235000000, + 188000000 + ], + "id": "Franz_Liszt-Hungarian_Rhapsody_No._2", + "tags": [ + "classic" + ], + "tilesCount": [ + 786, + 938, + 944 + ], + "title": "Hungarian Rhapsody No. 2", + "url": "songs/classic/Franz_Liszt-Hungarian_Rhapsody_No._2.mid" + }, + "Franz_Xaver_Gruber-Silent_Night": { + "__collections__": {}, + "artist": "Franz Xaver Gruber", + "bpm": 40, + "duration": [ + 171000000, + 128250000, + 102600000 + ], + "id": "Franz_Xaver_Gruber-Silent_Night", + "tags": [ + "other" + ], + "tilesCount": [ + 172, + 218, + 258 + ], + "title": "Silent Night", + "url": "songs/other/Franz_Xaver_Gruber-Silent_Night.mid" + }, + "Gary_Jules-Mad_World": { + "__collections__": {}, + "artist": "Gary Jules", + "bpm": 67, + "duration": [ + 302885572, + 227164179, + 181731343 + ], + "id": "Gary_Jules-Mad_World", + "tags": [ + "other" + ], + "tilesCount": [ + 651, + 718, + 719 + ], + "title": "Mad World", + "url": "songs/other/Gary_Jules-Mad_World.mid" + }, + "George_Gershwin-Rhapsody_In_Blue": { + "__collections__": {}, + "artist": "George Gershwin", + "bpm": 80, + "duration": [ + 964888889, + 723666667, + 578933333 + ], + "id": "George_Gershwin-Rhapsody_In_Blue", + "tags": [ + "classic" + ], + "tilesCount": [ + 1637, + 2015, + 2288 + ], + "title": "Rhapsody In Blue", + "url": "songs/classic/George_Gershwin-Rhapsody_In_Blue.mid" + }, + "Georges_Bizet-Habanera": { + "__collections__": {}, + "artist": "Georges Bizet", + "bpm": 120, + "duration": [ + 114666667, + 86000000, + 68800000 + ], + "id": "Georges_Bizet-Habanera", + "tags": [ + "classic" + ], + "tilesCount": [ + 374, + 477, + 526 + ], + "title": "Habanera", + "url": "songs/classic/Georges_Bizet-Habanera.mid" + }, + "Georges_Bizet-Les_Toradors": { + "__collections__": {}, + "artist": "Georges Bizet", + "bpm": 75, + "duration": [ + 366118519, + 274588889, + 219671111 + ], + "id": "Georges_Bizet-Les_Toradors", + "tags": [ + "classic" + ], + "tilesCount": [ + 1278, + 1568, + 1676 + ], + "title": "Les Toradors", + "url": "songs/classic/Georges_Bizet-Les_Toradors.mid" + }, + "Georges_Bizet-March_Of_The_Toreadors": { + "__collections__": {}, + "artist": "Georges Bizet", + "bpm": 120, + "duration": [ + 146537037, + 109902778, + 87922222 + ], + "id": "Georges_Bizet-March_Of_The_Toreadors", + "tags": [ + "classic" + ], + "tilesCount": [ + 375, + 504, + 591 + ], + "title": "March Of The Toreadors", + "url": "songs/classic/Georges_Bizet-March_Of_The_Toreadors.mid" + }, + "Giacomo_Puccini-Madama_Butterfly": { + "__collections__": {}, + "artist": "Giacomo Puccini", + "bpm": 75, + "duration": [ + 259992593, + 194994444, + 155995556 + ], + "id": "Giacomo_Puccini-Madama_Butterfly", + "tags": [ + "classic" + ], + "tilesCount": [ + 979, + 1147, + 1249 + ], + "title": "Madama Butterfly", + "url": "songs/classic/Giacomo_Puccini-Madama_Butterfly.mid" + }, + "Giacomo_Puccini-Nessun_Dorma": { + "__collections__": {}, + "artist": "Giacomo Puccini", + "bpm": 60, + "duration": [ + 703555556, + 527666667, + 422133333 + ], + "id": "Giacomo_Puccini-Nessun_Dorma", + "tags": [ + "classic" + ], + "tilesCount": [ + 702, + 842, + 959 + ], + "title": "Nessun Dorma", + "url": "songs/classic/Giacomo_Puccini-Nessun_Dorma.mid" + }, + "Giuseppe_Verdi-Drinking_Song": { + "__collections__": {}, + "artist": "Giuseppe Verdi", + "bpm": 75, + "duration": [ + 226844444, + 170133333, + 136106667 + ], + "id": "Giuseppe_Verdi-Drinking_Song", + "tags": [ + "classic" + ], + "tilesCount": [ + 365, + 365, + 365 + ], + "title": "Drinking Song", + "url": "songs/classic/Giuseppe_Verdi-Drinking_Song.mid" + }, + "Giuseppe_Verdi-La_Donna_E_Mobile": { + "__collections__": {}, + "artist": "Giuseppe Verdi", + "bpm": 100, + "duration": [ + 360533333, + 270400000, + 216320000 + ], + "id": "Giuseppe_Verdi-La_Donna_E_Mobile", + "tags": [ + "classic" + ], + "tilesCount": [ + 343, + 384, + 385 + ], + "title": "La Donna E Mobile", + "url": "songs/classic/Giuseppe_Verdi-La_Donna_E_Mobile.mid" + }, + "Giuseppe_Verdi-Triumphal_March": { + "__collections__": {}, + "artist": "Giuseppe Verdi", + "bpm": 109, + "duration": [ + 286238532, + 214678899, + 171743119 + ], + "id": "Giuseppe_Verdi-Triumphal_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 218, + 301, + 357 + ], + "title": "Triumphal March", + "url": "songs/classic/Giuseppe_Verdi-Triumphal_March.mid" + }, + "Gounod-Funeral_March_Of_A_Marionette": { + "__collections__": {}, + "artist": "Gounod", + "bpm": 109, + "duration": [ + 167584098, + 125688073, + 100550459 + ], + "id": "Gounod-Funeral_March_Of_A_Marionette", + "tags": [ + "classic" + ], + "tilesCount": [ + 441, + 513, + 547 + ], + "title": "Funeral March Of A Marionette", + "url": "songs/classic/Gounod-Funeral_March_Of_A_Marionette.mid" + }, + "Gustav_Mahler-Symphony_No._5_Mvt._4": { + "__collections__": {}, + "artist": "Gustav Mahler", + "bpm": 48, + "duration": [ + 666203704, + 499652778, + 399722222 + ], + "id": "Gustav_Mahler-Symphony_No._5_Mvt._4", + "tags": [ + "classic" + ], + "tilesCount": [ + 648, + 750, + 796 + ], + "title": "Symphony No. 5 Mvt. 4", + "url": "songs/classic/Gustav_Mahler-Symphony_No._5_Mvt._4.mid" + }, + "Handel-Arrival_Of_The_Queen_Of_Sheba": { + "__collections__": {}, + "artist": "Handel", + "bpm": 74, + "duration": [ + 384234234, + 288175676, + 230540541 + ], + "id": "Handel-Arrival_Of_The_Queen_Of_Sheba", + "tags": [ + "classic" + ], + "tilesCount": [ + 1671, + 1831, + 1832 + ], + "title": "Arrival Of The Queen Of Sheba", + "url": "songs/classic/Handel-Arrival_Of_The_Queen_Of_Sheba.mid" + }, + "Handel-Hallelujah_Chorus": { + "__collections__": {}, + "artist": "Handel", + "bpm": 104, + "duration": [ + 422820513, + 317115385, + 253692308 + ], + "id": "Handel-Hallelujah_Chorus", + "tags": [ + "classic" + ], + "tilesCount": [ + 1184, + 1603, + 1934 + ], + "title": "Hallelujah Chorus", + "url": "songs/classic/Handel-Hallelujah_Chorus.mid" + }, + "Handel-La_Rejouissance": { + "__collections__": {}, + "artist": "Handel", + "bpm": 120, + "duration": [ + 169555556, + 127166667, + 101733333 + ], + "id": "Handel-La_Rejouissance", + "tags": [ + "classic" + ], + "tilesCount": [ + 284, + 284, + 284 + ], + "title": "La Rejouissance", + "url": "songs/classic/Handel-La_Rejouissance.mid" + }, + "Handel-Ombra_Mai_Fu": { + "__collections__": {}, + "artist": "Handel", + "bpm": 55, + "duration": [ + 552727273, + 414545455, + 331636364 + ], + "id": "Handel-Ombra_Mai_Fu", + "tags": [ + "classic" + ], + "tilesCount": [ + 274, + 383, + 451 + ], + "title": "Ombra Mai Fu", + "url": "songs/classic/Handel-Ombra_Mai_Fu.mid" + }, + "Handel-Sarabande": { + "__collections__": {}, + "artist": "Handel", + "bpm": 120, + "duration": [ + 195111111, + 146333333, + 117066667 + ], + "id": "Handel-Sarabande", + "tags": [ + "classic" + ], + "tilesCount": [ + 384, + 461, + 502 + ], + "title": "Sarabande", + "url": "songs/classic/Handel-Sarabande.mid" + }, + "Handel-Zadok_The_Priest": { + "__collections__": {}, + "artist": "Handel", + "bpm": 72, + "duration": [ + 53518519, + 40138889, + 32111111 + ], + "id": "Handel-Zadok_The_Priest", + "tags": [ + "classic" + ], + "tilesCount": [ + 281, + 370, + 459 + ], + "title": "Zadok The Priest", + "url": "songs/classic/Handel-Zadok_The_Priest.mid" + }, + "Handel_Halvorsen-Passacaglia": { + "__collections__": {}, + "artist": "Handel Halvorsen", + "bpm": 130, + "duration": [ + 180717949, + 135538462, + 108430769 + ], + "id": "Handel_Halvorsen-Passacaglia", + "tags": [ + "other" + ], + "tilesCount": [ + 1022, + 1030, + 1030 + ], + "title": "Passacaglia", + "url": "songs/other/Handel_Halvorsen-Passacaglia.mid" + }, + "Hans_Zimmer-Interstellar,_Main_Theme": { + "__collections__": {}, + "artist": "Hans Zimmer", + "bpm": 74, + "duration": [ + 489099099, + 366824324, + 293459459 + ], + "id": "Hans_Zimmer-Interstellar,_Main_Theme", + "tags": [ + "other" + ], + "tilesCount": [ + 1864, + 1967, + 1973 + ], + "title": "Interstellar, Main Theme", + "url": "songs/other/Hans_Zimmer-Interstellar,_Main_Theme.mid" + }, + "Haydn-The_Nutcracker_-_Russian_Dance": { + "__collections__": {}, + "artist": "Haydn", + "bpm": 150, + "duration": [ + 197422222, + 148066667, + 118453333 + ], + "id": "Haydn-The_Nutcracker_-_Russian_Dance", + "tags": [ + "classic" + ], + "tilesCount": [ + 738, + 1017, + 1212 + ], + "title": "The Nutcracker - Russian Dance", + "url": "songs/classic/Haydn-The_Nutcracker_-_Russian_Dance.mid" + }, + "Haydn-Trumpet_Concerto": { + "__collections__": {}, + "artist": "Haydn", + "bpm": 120, + "duration": [ + 771444444, + 578583333, + 462866667 + ], + "id": "Haydn-Trumpet_Concerto", + "tags": [ + "classic" + ], + "tilesCount": [ + 2375, + 2973, + 3328 + ], + "title": "Trumpet Concerto", + "url": "songs/classic/Haydn-Trumpet_Concerto.mid" + }, + "Imagine_Dragons-Believer": { + "__collections__": {}, + "artist": "Imagine Dragons", + "bpm": 150, + "duration": [ + 318488889, + 238866667, + 191093333 + ], + "id": "Imagine_Dragons-Believer", + "tags": [ + "pop" + ], + "tilesCount": [ + 1262, + 1441, + 1468 + ], + "title": "Believer", + "url": "songs/pop/Imagine_Dragons-Believer.mid" + }, + "Italian_Folk-Bella_Ciao": { + "__collections__": {}, + "artist": "Italian Folk", + "bpm": 120, + "duration": [ + 153333333, + 115000000, + 92000000 + ], + "id": "Italian_Folk-Bella_Ciao", + "tags": [ + "folk" + ], + "tilesCount": [ + 843, + 1095, + 1178 + ], + "title": "Bella Ciao", + "url": "songs/folk/Italian_Folk-Bella_Ciao.mid" + }, + "Jacques_Offenbach-Barcarolle": { + "__collections__": {}, + "artist": "Jacques Offenbach", + "bpm": 80, + "duration": [ + 565333333, + 424000000, + 339200000 + ], + "id": "Jacques_Offenbach-Barcarolle", + "tags": [ + "classic" + ], + "tilesCount": [ + 823, + 896, + 912 + ], + "title": "Barcarolle", + "url": "songs/classic/Jacques_Offenbach-Barcarolle.mid" + }, + "Jacques_Offenbach-Orpheus_In_The_Underworld": { + "__collections__": {}, + "artist": "Jacques Offenbach", + "bpm": 145, + "duration": [ + 173977011, + 130482759, + 104386207 + ], + "id": "Jacques_Offenbach-Orpheus_In_The_Underworld", + "tags": [ + "classic" + ], + "tilesCount": [ + 996, + 1454, + 1780 + ], + "title": "Orpheus In The Underworld", + "url": "songs/classic/Jacques_Offenbach-Orpheus_In_The_Underworld.mid" + }, + "Janji-Heroes_Tonight": { + "__collections__": {}, + "artist": "Janji", + "bpm": 120, + "duration": [ + 291222222, + 218416667, + 174733333 + ], + "id": "Janji-Heroes_Tonight", + "tags": [ + "pop" + ], + "tilesCount": [ + 1064, + 1311, + 1420 + ], + "title": "Heroes Tonight", + "url": "songs/pop/Janji-Heroes_Tonight.mid" + }, + "Japanese_Folk-Sakura_Sakura": { + "__collections__": {}, + "artist": "Japanese Folk", + "bpm": 120, + "duration": [ + 73111111, + 54833333, + 43866667 + ], + "id": "Japanese_Folk-Sakura_Sakura", + "tags": [ + "folk" + ], + "tilesCount": [ + 222, + 240, + 240 + ], + "title": "Sakura Sakura", + "url": "songs/folk/Japanese_Folk-Sakura_Sakura.mid" + }, + "Joe_Hisaishi-Innocent": { + "__collections__": {}, + "artist": "Joe Hisaishi", + "bpm": 100, + "duration": [ + 220594444, + 165445833, + 132356667 + ], + "id": "Joe_Hisaishi-Innocent", + "tags": [ + "other" + ], + "tilesCount": [ + 403, + 453, + 485 + ], + "title": "Innocent", + "url": "songs/other/Joe_Hisaishi-Innocent.mid" + }, + "Joe_Hisaishi-Merry_Go_Round_of_Life,_Howls_Moving_Castle": { + "__collections__": {}, + "artist": "Joe Hisaishi", + "bpm": 100, + "duration": [ + 2071789444, + 1553842083, + 1243073667 + ], + "id": "Joe_Hisaishi-Merry_Go_Round_of_Life,_Howls_Moving_Castle", + "tags": [ + "other" + ], + "tilesCount": [ + 1578, + 1988, + 2247 + ], + "title": "Merry Go Round of Life, Howls Moving Castle", + "url": "songs/other/Joe_Hisaishi-Merry_Go_Round_of_Life,_Howls_Moving_Castle.mid" + }, + "Joe_Hisaishi-One_Summers_Day,_Spirited_Away": { + "__collections__": {}, + "artist": "Joe Hisaishi", + "bpm": 77, + "duration": [ + 478229437, + 358672078, + 286937662 + ], + "id": "Joe_Hisaishi-One_Summers_Day,_Spirited_Away", + "tags": [ + "other" + ], + "tilesCount": [ + 731, + 866, + 913 + ], + "title": "One Summers Day, Spirited Away", + "url": "songs/other/Joe_Hisaishi-One_Summers_Day,_Spirited_Away.mid" + }, + "Joe_Hisaishi-Summer": { + "__collections__": {}, + "artist": "Joe Hisaishi", + "bpm": 75, + "duration": [ + 233177778, + 174883333, + 139906667 + ], + "id": "Joe_Hisaishi-Summer", + "tags": [ + "other" + ], + "tilesCount": [ + 930, + 1096, + 1201 + ], + "title": "Summer", + "url": "songs/other/Joe_Hisaishi-Summer.mid" + }, + "Johann_Strauss-Blue_Danube_Waltz": { + "__collections__": {}, + "artist": "Johann Strauss", + "bpm": 115, + "duration": [ + 1082579710, + 811934783, + 649547826 + ], + "id": "Johann_Strauss-Blue_Danube_Waltz", + "tags": [ + "classic" + ], + "tilesCount": [ + 3035, + 3934, + 4413 + ], + "title": "Blue Danube Waltz", + "url": "songs/classic/Johann_Strauss-Blue_Danube_Waltz.mid" + }, + "Johann_Strauss-Die_Fledermaus": { + "__collections__": {}, + "artist": "Johann Strauss", + "bpm": 150, + "duration": [ + 981155556, + 735866667, + 588693333 + ], + "id": "Johann_Strauss-Die_Fledermaus", + "tags": [ + "classic" + ], + "tilesCount": [ + 3633, + 4355, + 4686 + ], + "title": "Die Fledermaus", + "url": "songs/classic/Johann_Strauss-Die_Fledermaus.mid" + }, + "Johann_Strauss-Emperor_Waltz": { + "__collections__": {}, + "artist": "Johann Strauss", + "bpm": 120, + "duration": [ + 178444444, + 133833333, + 107066667 + ], + "id": "Johann_Strauss-Emperor_Waltz", + "tags": [ + "classic" + ], + "tilesCount": [ + 257, + 270, + 272 + ], + "title": "Emperor Waltz", + "url": "songs/classic/Johann_Strauss-Emperor_Waltz.mid" + }, + "Johann_Strauss-Radetzky_March": { + "__collections__": {}, + "artist": "Johann Strauss", + "bpm": 200, + "duration": [ + 850933333, + 638200000, + 510560000 + ], + "id": "Johann_Strauss-Radetzky_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 1201, + 1457, + 1649 + ], + "title": "Radetzky March", + "url": "songs/classic/Johann_Strauss-Radetzky_March.mid" + }, + "Johann_Strauss-Tritsch_Tratsch_Polka": { + "__collections__": {}, + "artist": "Johann Strauss", + "bpm": 150, + "duration": [ + 268503704, + 201377778, + 161102222 + ], + "id": "Johann_Strauss-Tritsch_Tratsch_Polka", + "tags": [ + "classic" + ], + "tilesCount": [ + 1128, + 1343, + 1418 + ], + "title": "Tritsch Tratsch Polka", + "url": "songs/classic/Johann_Strauss-Tritsch_Tratsch_Polka.mid" + }, + "Johann_Strauss-Voices_Of_Spring": { + "__collections__": {}, + "artist": "Johann Strauss", + "bpm": 149, + "duration": [ + 1196420582, + 897315436, + 717852349 + ], + "id": "Johann_Strauss-Voices_Of_Spring", + "tags": [ + "classic" + ], + "tilesCount": [ + 3661, + 4507, + 4875 + ], + "title": "Voices Of Spring", + "url": "songs/classic/Johann_Strauss-Voices_Of_Spring.mid" + }, + "Johann_Strauss-Wiener_Blut": { + "__collections__": {}, + "artist": "Johann Strauss", + "bpm": 120, + "duration": [ + 180444444, + 135333333, + 108266667 + ], + "id": "Johann_Strauss-Wiener_Blut", + "tags": [ + "classic" + ], + "tilesCount": [ + 370, + 370, + 370 + ], + "title": "Wiener Blut", + "url": "songs/classic/Johann_Strauss-Wiener_Blut.mid" + }, + "Johannes_Brahms-Hungarian_Dance_No_5": { + "__collections__": {}, + "artist": "Johannes Brahms", + "bpm": 130, + "duration": [ + 296102564, + 222076923, + 177661538 + ], + "id": "Johannes_Brahms-Hungarian_Dance_No_5", + "tags": [ + "classic" + ], + "tilesCount": [ + 1118, + 1450, + 1621 + ], + "title": "Hungarian Dance No 5", + "url": "songs/classic/Johannes_Brahms-Hungarian_Dance_No_5.mid" + }, + "Johannes_Brahms-Hungarian_Dance_No_6": { + "__collections__": {}, + "artist": "Johannes Brahms", + "bpm": 80, + "duration": [ + 563000000, + 422250000, + 337800000 + ], + "id": "Johannes_Brahms-Hungarian_Dance_No_6", + "tags": [ + "classic" + ], + "tilesCount": [ + 1362, + 1846, + 2198 + ], + "title": "Hungarian Dance No 6", + "url": "songs/classic/Johannes_Brahms-Hungarian_Dance_No_6.mid" + }, + "John_Legend-All_Of_Me": { + "__collections__": {}, + "artist": "John Legend", + "bpm": 120, + "duration": [ + 382444444, + 286833333, + 229466667 + ], + "id": "John_Legend-All_Of_Me", + "tags": [ + "pop" + ], + "tilesCount": [ + 1026, + 1238, + 1296 + ], + "title": "All Of Me", + "url": "songs/pop/John_Legend-All_Of_Me.mid" + }, + "John_Lennon-Imagine": { + "__collections__": {}, + "artist": "John Lennon", + "bpm": 70, + "duration": [ + 345333333, + 259000000, + 207200000 + ], + "id": "John_Lennon-Imagine", + "tags": [ + "pop" + ], + "tilesCount": [ + 742, + 827, + 831 + ], + "title": "Imagine", + "url": "songs/pop/John_Lennon-Imagine.mid" + }, + "John_Philip_Sousa-Liberty_Bell": { + "__collections__": {}, + "artist": "John Philip Sousa", + "bpm": 150, + "duration": [ + 348918519, + 261688889, + 209351111 + ], + "id": "John_Philip_Sousa-Liberty_Bell", + "tags": [ + "classic" + ], + "tilesCount": [ + 1724, + 2347, + 2740 + ], + "title": "Liberty Bell", + "url": "songs/classic/John_Philip_Sousa-Liberty_Bell.mid" + }, + "John_Philip_Sousa-Semper_Fidelis": { + "__collections__": {}, + "artist": "John Philip Sousa", + "bpm": 149, + "duration": [ + 216435496, + 162326622, + 129861298 + ], + "id": "John_Philip_Sousa-Semper_Fidelis", + "tags": [ + "classic" + ], + "tilesCount": [ + 1065, + 1418, + 1672 + ], + "title": "Semper Fidelis", + "url": "songs/classic/John_Philip_Sousa-Semper_Fidelis.mid" + }, + "John_Philip_Sousa-Stars_And_Stripes_Forever": { + "__collections__": {}, + "artist": "John Philip Sousa", + "bpm": 240, + "duration": [ + 480888889, + 360666667, + 288533333 + ], + "id": "John_Philip_Sousa-Stars_And_Stripes_Forever", + "tags": [ + "classic" + ], + "tilesCount": [ + 1687, + 2205, + 2402 + ], + "title": "Stars And Stripes Forever", + "url": "songs/classic/John_Philip_Sousa-Stars_And_Stripes_Forever.mid" + }, + "Jose_Feliciano-Feliz_Navidad": { + "__collections__": {}, + "artist": "Jose Feliciano", + "bpm": 120, + "duration": [ + 89444444, + 67083333, + 53666667 + ], + "id": "Jose_Feliciano-Feliz_Navidad", + "tags": [ + "other" + ], + "tilesCount": [ + 245, + 297, + 320 + ], + "title": "Feliz Navidad", + "url": "songs/other/Jose_Feliciano-Feliz_Navidad.mid" + }, + "Julius_Fucik-Entry_Of_The_Gladiators": { + "__collections__": {}, + "artist": "Julius Fucik", + "bpm": 200, + "duration": [ + 1730933333, + 1298200000, + 1038560000 + ], + "id": "Julius_Fucik-Entry_Of_The_Gladiators", + "tags": [ + "classic" + ], + "tilesCount": [ + 1933, + 2507, + 2788 + ], + "title": "Entry Of The Gladiators", + "url": "songs/classic/Julius_Fucik-Entry_Of_The_Gladiators.mid" + }, + "Juventino_Rosas-Over_The_Waves": { + "__collections__": {}, + "artist": "Juventino Rosas", + "bpm": 120, + "duration": [ + 765777778, + 574333333, + 459466667 + ], + "id": "Juventino_Rosas-Over_The_Waves", + "tags": [ + "classic" + ], + "tilesCount": [ + 1750, + 2124, + 2152 + ], + "title": "Over The Waves", + "url": "songs/classic/Juventino_Rosas-Over_The_Waves.mid" + }, + "Karl_Jenkins-Palladio": { + "__collections__": {}, + "artist": "Karl Jenkins", + "bpm": 75, + "duration": [ + 323644444, + 242733333, + 194186667 + ], + "id": "Karl_Jenkins-Palladio", + "tags": [ + "classic" + ], + "tilesCount": [ + 1778, + 2264, + 2635 + ], + "title": "Palladio", + "url": "songs/classic/Karl_Jenkins-Palladio.mid" + }, + "Kazumi_Totaka-Mii_Channel": { + "__collections__": {}, + "artist": "Kazumi Totaka", + "bpm": 113, + "duration": [ + 142064897, + 106548673, + 85238938 + ], + "id": "Kazumi_Totaka-Mii_Channel", + "tags": [ + "other" + ], + "tilesCount": [ + 452, + 538, + 578 + ], + "title": "Mii Channel", + "url": "songs/other/Kazumi_Totaka-Mii_Channel.mid" + }, + "Ketelbey-In_A_Persian_Market": { + "__collections__": {}, + "artist": "Ketelbey", + "bpm": 89, + "duration": [ + 1234019975, + 925514981, + 740411985 + ], + "id": "Ketelbey-In_A_Persian_Market", + "tags": [ + "classic" + ], + "tilesCount": [ + 2175, + 2611, + 2883 + ], + "title": "In A Persian Market", + "url": "songs/classic/Ketelbey-In_A_Persian_Market.mid" + }, + "Kimi_No_Na_Wa-Sparkle": { + "__collections__": {}, + "artist": "Kimi No Na Wa", + "bpm": 150, + "duration": [ + 674592222, + 505944167, + 404755333 + ], + "id": "Kimi_No_Na_Wa-Sparkle", + "tags": [ + "other" + ], + "tilesCount": [ + 2472, + 2751, + 2894 + ], + "title": "Sparkle", + "url": "songs/other/Kimi_No_Na_Wa-Sparkle.mid" + }, + "Korean_Folk-Arirang": { + "__collections__": {}, + "artist": "Korean Folk", + "bpm": 80, + "duration": [ + 142666667, + 107000000, + 85600000 + ], + "id": "Korean_Folk-Arirang", + "tags": [ + "folk" + ], + "tilesCount": [ + 408, + 408, + 408 + ], + "title": "Arirang", + "url": "songs/folk/Korean_Folk-Arirang.mid" + }, + "Leonard_Cohen-Hallelujah": { + "__collections__": {}, + "artist": "Leonard Cohen", + "bpm": 89, + "duration": [ + 165425094, + 124068820, + 99255056 + ], + "id": "Leonard_Cohen-Hallelujah", + "tags": [ + "other" + ], + "tilesCount": [ + 482, + 508, + 508 + ], + "title": "Hallelujah", + "url": "songs/other/Leonard_Cohen-Hallelujah.mid" + }, + "Lewis_Capaldi-Someone_You_Loved": { + "__collections__": {}, + "artist": "Lewis Capaldi", + "bpm": 109, + "duration": [ + 239755352, + 179816514, + 143853211 + ], + "id": "Lewis_Capaldi-Someone_You_Loved", + "tags": [ + "other" + ], + "tilesCount": [ + 1051, + 1281, + 1419 + ], + "title": "Someone You Loved", + "url": "songs/other/Lewis_Capaldi-Someone_You_Loved.mid" + }, + "Liszt-Etude_In_G_Minor_La_Campanella": { + "__collections__": {}, + "artist": "Liszt", + "bpm": 75, + "duration": [ + 734718519, + 551038889, + 440831111 + ], + "id": "Liszt-Etude_In_G_Minor_La_Campanella", + "tags": [ + "classic" + ], + "tilesCount": [ + 3150, + 3647, + 3964 + ], + "title": "Etude In G Minor La Campanella", + "url": "songs/classic/Liszt-Etude_In_G_Minor_La_Campanella.mid" + }, + "Liszt-Liebestraum_No._3": { + "__collections__": {}, + "artist": "Liszt", + "bpm": 149, + "duration": [ + 341953766, + 256465324, + 205172260 + ], + "id": "Liszt-Liebestraum_No._3", + "tags": [ + "classic" + ], + "tilesCount": [ + 1578, + 1786, + 1860 + ], + "title": "Liebestraum No. 3", + "url": "songs/classic/Liszt-Liebestraum_No._3.mid" + }, + "Ludovico_Einaudi-Experience": { + "__collections__": {}, + "artist": "Ludovico Einaudi", + "bpm": 72, + "duration": [ + 499351852, + 374513889, + 299611111 + ], + "id": "Ludovico_Einaudi-Experience", + "tags": [ + "other" + ], + "tilesCount": [ + 2534, + 2572, + 2597 + ], + "title": "Experience", + "url": "songs/other/Ludovico_Einaudi-Experience.mid" + }, + "Ludovico_Einaudi-Una_Mattina": { + "__collections__": {}, + "artist": "Ludovico Einaudi", + "bpm": 60, + "duration": [ + 267222222, + 200416667, + 160333333 + ], + "id": "Ludovico_Einaudi-Una_Mattina", + "tags": [ + "other" + ], + "tilesCount": [ + 776, + 776, + 776 + ], + "title": "Una Mattina", + "url": "songs/other/Ludovico_Einaudi-Una_Mattina.mid" + }, + "Ludovico_Einoudi-Nuvole_Bianche": { + "__collections__": {}, + "artist": "Ludovico Einoudi", + "bpm": 40, + "duration": [ + 1204777778, + 903583333, + 722866667 + ], + "id": "Ludovico_Einoudi-Nuvole_Bianche", + "tags": [ + "other" + ], + "tilesCount": [ + 1656, + 1707, + 1740 + ], + "title": "Nuvole Bianche", + "url": "songs/other/Ludovico_Einoudi-Nuvole_Bianche.mid" + }, + "Luigi_Denza-Funiculi_Funicula": { + "__collections__": {}, + "artist": "Luigi Denza", + "bpm": 150, + "duration": [ + 119911111, + 89933333, + 71946667 + ], + "id": "Luigi_Denza-Funiculi_Funicula", + "tags": [ + "classic" + ], + "tilesCount": [ + 164, + 216, + 244 + ], + "title": "Funiculi Funicula", + "url": "songs/classic/Luigi_Denza-Funiculi_Funicula.mid" + }, + "Luis_Fonsi_ft_Daddy_Yankee-Despacito": { + "__collections__": {}, + "artist": "Luis Fonsi ft Daddy Yankee", + "bpm": 75, + "duration": [ + 367274074, + 275455556, + 220364444 + ], + "id": "Luis_Fonsi_ft_Daddy_Yankee-Despacito", + "tags": [ + "pop" + ], + "tilesCount": [ + 1537, + 1723, + 1747 + ], + "title": "Despacito", + "url": "songs/pop/Luis_Fonsi_ft_Daddy_Yankee-Despacito.mid" + }, + "Mariah_Carey-All_I_Want_For_Christmas_Is_You": { + "__collections__": {}, + "artist": "Mariah Carey", + "bpm": 100, + "duration": [ + 472613333, + 354460000, + 283568000 + ], + "id": "Mariah_Carey-All_I_Want_For_Christmas_Is_You", + "tags": [ + "pop" + ], + "tilesCount": [ + 1758, + 2224, + 2443 + ], + "title": "All I Want For Christmas Is You", + "url": "songs/pop/Mariah_Carey-All_I_Want_For_Christmas_Is_You.mid" + }, + "Megalovania-Undertale": { + "__collections__": {}, + "artist": "Megalovania", + "bpm": 75, + "duration": [ + 338970370, + 254227778, + 203382222 + ], + "id": "Megalovania-Undertale", + "tags": [ + "other" + ], + "tilesCount": [ + 1506, + 1711, + 1811 + ], + "title": "Undertale", + "url": "songs/other/Megalovania-Undertale.mid" + }, + "Mendelssohn_Bartholdy-Wedding_March": { + "__collections__": {}, + "artist": "Mendelssohn Bartholdy", + "bpm": 37, + "duration": [ + 344009009, + 258006757, + 206405405 + ], + "id": "Mendelssohn_Bartholdy-Wedding_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 414, + 515, + 559 + ], + "title": "Wedding March", + "url": "songs/classic/Mendelssohn_Bartholdy-Wedding_March.mid" + }, + "Meredith_Willson-It's_Beginning_To_Look_A_Lot_Like_Christmas": { + "__collections__": {}, + "artist": "Meredith Willson", + "bpm": 120, + "duration": [ + 64000000, + 48000000, + 38400000 + ], + "id": "Meredith_Willson-It's_Beginning_To_Look_A_Lot_Like_Christmas", + "tags": [ + "other" + ], + "tilesCount": [ + 173, + 192, + 200 + ], + "title": "It's Beginning To Look A Lot Like Christmas", + "url": "songs/other/Meredith_Willson-It's_Beginning_To_Look_A_Lot_Like_Christmas.mid" + }, + "Michael_Buble-White_Christmas": { + "__collections__": {}, + "artist": "Michael Buble", + "bpm": 120, + "duration": [ + 94666667, + 71000000, + 56800000 + ], + "id": "Michael_Buble-White_Christmas", + "tags": [ + "other" + ], + "tilesCount": [ + 153, + 165, + 165 + ], + "title": "White Christmas", + "url": "songs/other/Michael_Buble-White_Christmas.mid" + }, + "Mozart-Aria_Queen_Of_The_Night": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 140, + "duration": [ + 326619048, + 244964286, + 195971429 + ], + "id": "Mozart-Aria_Queen_Of_The_Night", + "tags": [ + "classic" + ], + "tilesCount": [ + 1166, + 1296, + 1311 + ], + "title": "Aria Queen Of The Night", + "url": "songs/classic/Mozart-Aria_Queen_Of_The_Night.mid" + }, + "Mozart-Eine_Kleine_Nachtmusik": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 150, + "duration": [ + 211822222, + 158866667, + 127093333 + ], + "id": "Mozart-Eine_Kleine_Nachtmusik", + "tags": [ + "classic" + ], + "tilesCount": [ + 840, + 1074, + 1155 + ], + "title": "Eine Kleine Nachtmusik", + "url": "songs/classic/Mozart-Eine_Kleine_Nachtmusik.mid" + }, + "Mozart-Lacrimosa": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 120, + "duration": [ + 117111111, + 87833333, + 70266667 + ], + "id": "Mozart-Lacrimosa", + "tags": [ + "classic" + ], + "tilesCount": [ + 491, + 603, + 679 + ], + "title": "Lacrimosa", + "url": "songs/classic/Mozart-Lacrimosa.mid" + }, + "Mozart-Marriage_Of_Figaro": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 150, + "duration": [ + 661511111, + 496133333, + 396906667 + ], + "id": "Mozart-Marriage_Of_Figaro", + "tags": [ + "classic" + ], + "tilesCount": [ + 2473, + 3107, + 3506 + ], + "title": "Marriage Of Figaro", + "url": "songs/classic/Mozart-Marriage_Of_Figaro.mid" + }, + "Mozart-Piano_Sonata_No._11": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 75, + "duration": [ + 573488889, + 430116667, + 344093333 + ], + "id": "Mozart-Piano_Sonata_No._11", + "tags": [ + "classic" + ], + "tilesCount": [ + 2346, + 2787, + 2823 + ], + "title": "Piano Sonata No. 11", + "url": "songs/classic/Mozart-Piano_Sonata_No._11.mid" + }, + "Mozart-Requiem": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 64, + "duration": [ + 219583333, + 164687500, + 131750000 + ], + "id": "Mozart-Requiem", + "tags": [ + "classic" + ], + "tilesCount": [ + 491, + 603, + 679 + ], + "title": "Requiem", + "url": "songs/classic/Mozart-Requiem.mid" + }, + "Mozart-Rondo_Alla_Turca": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 75, + "duration": [ + 571333333, + 428500000, + 342800000 + ], + "id": "Mozart-Rondo_Alla_Turca", + "tags": [ + "classic" + ], + "tilesCount": [ + 2344, + 2785, + 2823 + ], + "title": "Rondo Alla Turca", + "url": "songs/classic/Mozart-Rondo_Alla_Turca.mid" + }, + "Mozart-Symphony_No._25": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 150, + "duration": [ + 854844444, + 641133333, + 512906667 + ], + "id": "Mozart-Symphony_No._25", + "tags": [ + "classic" + ], + "tilesCount": [ + 3537, + 4311, + 4551 + ], + "title": "Symphony No. 25", + "url": "songs/classic/Mozart-Symphony_No._25.mid" + }, + "Mozart-Symphony_No._40_in_G": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 149, + "duration": [ + 442953020, + 332214765, + 265771812 + ], + "id": "Mozart-Symphony_No._40_in_G", + "tags": [ + "classic" + ], + "tilesCount": [ + 2074, + 2394, + 2480 + ], + "title": "Symphony No. 40 in G", + "url": "songs/classic/Mozart-Symphony_No._40_in_G.mid" + }, + "Mozart-Turkish_March": { + "__collections__": {}, + "artist": "Mozart", + "bpm": 75, + "duration": [ + 565488889, + 424116667, + 339293333 + ], + "id": "Mozart-Turkish_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 2324, + 2767, + 2803 + ], + "title": "Turkish March", + "url": "songs/classic/Mozart-Turkish_March.mid" + }, + "Mr_Grinch-You're_A_Mean_One": { + "__collections__": {}, + "artist": "Mr Grinch", + "bpm": 150, + "duration": [ + 405870370, + 304402778, + 243522222 + ], + "id": "Mr_Grinch-You're_A_Mean_One", + "tags": [ + "other" + ], + "tilesCount": [ + 1245, + 1582, + 1775 + ], + "title": "You're A Mean One", + "url": "songs/other/Mr_Grinch-You're_A_Mean_One.mid" + }, + "Mykola_Leontovych-Carol_Of_The_Bells": { + "__collections__": {}, + "artist": "Mykola Leontovych", + "bpm": 150, + "duration": [ + 196566667, + 147425000, + 117940000 + ], + "id": "Mykola_Leontovych-Carol_Of_The_Bells", + "tags": [ + "other" + ], + "tilesCount": [ + 974, + 1138, + 1234 + ], + "title": "Carol Of The Bells", + "url": "songs/other/Mykola_Leontovych-Carol_Of_The_Bells.mid" + }, + "One_Republic-Counting_Stars": { + "__collections__": {}, + "artist": "One Republic", + "bpm": 120, + "duration": [ + 42555556, + 31916667, + 25533333 + ], + "id": "One_Republic-Counting_Stars", + "tags": [ + "pop" + ], + "tilesCount": [ + 102, + 110, + 110 + ], + "title": "Counting Stars", + "url": "songs/pop/One_Republic-Counting_Stars.mid" + }, + "Pachelbel-Canon_In_D": { + "__collections__": {}, + "artist": "Pachelbel", + "bpm": 100, + "duration": [ + 482688333, + 362016250, + 289613000 + ], + "id": "Pachelbel-Canon_In_D", + "tags": [ + "other" + ], + "tilesCount": [ + 1490, + 1566, + 1589 + ], + "title": "Canon In D", + "url": "songs/other/Pachelbel-Canon_In_D.mid" + }, + "Paganini-Caprice_No._24": { + "__collections__": {}, + "artist": "Paganini", + "bpm": 75, + "duration": [ + 237474074, + 178105556, + 142484444 + ], + "id": "Paganini-Caprice_No._24", + "tags": [ + "classic" + ], + "tilesCount": [ + 961, + 1168, + 1238 + ], + "title": "Caprice No. 24", + "url": "songs/classic/Paganini-Caprice_No._24.mid" + }, + "Paul_De_Senneville-Mariage_d'Amour": { + "__collections__": {}, + "artist": "Paul De Senneville", + "bpm": 75, + "duration": [ + 363037778, + 272278333, + 217822667 + ], + "id": "Paul_De_Senneville-Mariage_d'Amour", + "tags": [ + "other" + ], + "tilesCount": [ + 1618, + 1623, + 1625 + ], + "title": "Mariage d'Amour", + "url": "songs/other/Paul_De_Senneville-Mariage_d'Amour.mid" + }, + "Ponchielli-Dance_Of_The_Hours": { + "__collections__": {}, + "artist": "Ponchielli", + "bpm": 149, + "duration": [ + 300134228, + 225100671, + 180080537 + ], + "id": "Ponchielli-Dance_Of_The_Hours", + "tags": [ + "classic" + ], + "tilesCount": [ + 1251, + 1605, + 1770 + ], + "title": "Dance Of The Hours", + "url": "songs/classic/Ponchielli-Dance_Of_The_Hours.mid" + }, + "Purcell-Funeral_Of_Queen_Mary": { + "__collections__": {}, + "artist": "Purcell", + "bpm": 75, + "duration": [ + 136977778, + 102733333, + 82186667 + ], + "id": "Purcell-Funeral_Of_Queen_Mary", + "tags": [ + "classic" + ], + "tilesCount": [ + 150, + 179, + 207 + ], + "title": "Funeral Of Queen Mary", + "url": "songs/classic/Purcell-Funeral_Of_Queen_Mary.mid" + }, + "Rachmaninoff-Prelude_In_C_Sharp_Minor": { + "__collections__": {}, + "artist": "Rachmaninoff", + "bpm": 40, + "duration": [ + 495851852, + 371888889, + 297511111 + ], + "id": "Rachmaninoff-Prelude_In_C_Sharp_Minor", + "tags": [ + "classic" + ], + "tilesCount": [ + 932, + 1197, + 1346 + ], + "title": "Prelude In C Sharp Minor", + "url": "songs/classic/Rachmaninoff-Prelude_In_C_Sharp_Minor.mid" + }, + "Rachmaninoff-Rhapsody_On_A_Theme_Of_Paganini": { + "__collections__": {}, + "artist": "Rachmaninoff", + "bpm": 60, + "duration": [ + 218641975, + 163981481, + 131185185 + ], + "id": "Rachmaninoff-Rhapsody_On_A_Theme_Of_Paganini", + "tags": [ + "classic" + ], + "tilesCount": [ + 652, + 831, + 961 + ], + "title": "Rhapsody On A Theme Of Paganini", + "url": "songs/classic/Rachmaninoff-Rhapsody_On_A_Theme_Of_Paganini.mid" + }, + "Ramin_Djawadi-Game_Of_Thrones": { + "__collections__": {}, + "artist": "Ramin Djawadi", + "bpm": 150, + "duration": [ + 146488889, + 109866667, + 87893333 + ], + "id": "Ramin_Djawadi-Game_Of_Thrones", + "tags": [ + "other" + ], + "tilesCount": [ + 523, + 646, + 661 + ], + "title": "Game Of Thrones", + "url": "songs/other/Ramin_Djawadi-Game_Of_Thrones.mid" + }, + "Respighi-Pines_Of_The_Appian_Way": { + "__collections__": {}, + "artist": "Respighi", + "bpm": 120, + "duration": [ + 105888889, + 79416667, + 63533333 + ], + "id": "Respighi-Pines_Of_The_Appian_Way", + "tags": [ + "classic" + ], + "tilesCount": [ + 317, + 366, + 385 + ], + "title": "Pines Of The Appian Way", + "url": "songs/classic/Respighi-Pines_Of_The_Appian_Way.mid" + }, + "Richard_Clayderman-Ballade_Pour_Adeline": { + "__collections__": {}, + "artist": "Richard Clayderman", + "bpm": 60, + "duration": [ + 257733333, + 193300000, + 154640000 + ], + "id": "Richard_Clayderman-Ballade_Pour_Adeline", + "tags": [ + "other" + ], + "tilesCount": [ + 782, + 883, + 894 + ], + "title": "Ballade Pour Adeline", + "url": "songs/other/Richard_Clayderman-Ballade_Pour_Adeline.mid" + }, + "Richard_Wagner-Tristan_And_Isolde": { + "__collections__": {}, + "artist": "Richard Wagner", + "bpm": 37, + "duration": [ + 707042042, + 530281532, + 424225225 + ], + "id": "Richard_Wagner-Tristan_And_Isolde", + "tags": [ + "classic" + ], + "tilesCount": [ + 2480, + 2814, + 2943 + ], + "title": "Tristan And Isolde", + "url": "songs/classic/Richard_Wagner-Tristan_And_Isolde.mid" + }, + "Rick_Astley-Never_Gonna_Give_You_Up": { + "__collections__": {}, + "artist": "Rick Astley", + "bpm": 113, + "duration": [ + 452743363, + 339557522, + 271646018 + ], + "id": "Rick_Astley-Never_Gonna_Give_You_Up", + "tags": [ + "other" + ], + "tilesCount": [ + 1133, + 1353, + 1519 + ], + "title": "Never Gonna Give You Up", + "url": "songs/other/Rick_Astley-Never_Gonna_Give_You_Up.mid" + }, + "Ricketts-Colonel_Bogey_March": { + "__collections__": {}, + "artist": "Ricketts", + "bpm": 207, + "duration": [ + 819838969, + 614879227, + 491903382 + ], + "id": "Ricketts-Colonel_Bogey_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 2081, + 2675, + 3016 + ], + "title": "Colonel Bogey March", + "url": "songs/classic/Ricketts-Colonel_Bogey_March.mid" + }, + "Rimsky_Korsakov-Flight_Of_The_Bumblebee": { + "__collections__": {}, + "artist": "Rimsky Korsakov", + "bpm": 75, + "duration": [ + 220888889, + 165666667, + 132533333 + ], + "id": "Rimsky_Korsakov-Flight_Of_The_Bumblebee", + "tags": [ + "classic" + ], + "tilesCount": [ + 981, + 1069, + 1125 + ], + "title": "Flight Of The Bumblebee", + "url": "songs/classic/Rimsky_Korsakov-Flight_Of_The_Bumblebee.mid" + }, + "Rossini-Barber_Of_Seville_-_Overture": { + "__collections__": {}, + "artist": "Rossini", + "bpm": 80, + "duration": [ + 136000000, + 102000000, + 81600000 + ], + "id": "Rossini-Barber_Of_Seville_-_Overture", + "tags": [ + "classic" + ], + "tilesCount": [ + 281, + 353, + 383 + ], + "title": "Barber Of Seville - Overture", + "url": "songs/classic/Rossini-Barber_Of_Seville_-_Overture.mid" + }, + "Rossini-William_Tell_Overture_Finale": { + "__collections__": {}, + "artist": "Rossini", + "bpm": 75, + "duration": [ + 477511111, + 358133333, + 286506667 + ], + "id": "Rossini-William_Tell_Overture_Finale", + "tags": [ + "classic" + ], + "tilesCount": [ + 2153, + 2748, + 3018 + ], + "title": "William Tell Overture Finale", + "url": "songs/classic/Rossini-William_Tell_Overture_Finale.mid" + }, + "Russian_Folk-Kalinka": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 120, + "duration": [ + 75111111, + 56333333, + 45066667 + ], + "id": "Russian_Folk-Kalinka", + "tags": [ + "folk" + ], + "tilesCount": [ + 210, + 269, + 328 + ], + "title": "Kalinka", + "url": "songs/folk/Russian_Folk-Kalinka.mid" + }, + "Russian_Folk-Katyusha": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 120, + "duration": [ + 151555556, + 113666667, + 90933333 + ], + "id": "Russian_Folk-Katyusha", + "tags": [ + "folk" + ], + "tilesCount": [ + 653, + 794, + 855 + ], + "title": "Katyusha", + "url": "songs/folk/Russian_Folk-Katyusha.mid" + }, + "Russian_Folk-Korobeiniki": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 125, + "duration": [ + 104320000, + 78240000, + 62592000 + ], + "id": "Russian_Folk-Korobeiniki", + "tags": [ + "folk" + ], + "tilesCount": [ + 473, + 483, + 493 + ], + "title": "Korobeiniki", + "url": "songs/folk/Russian_Folk-Korobeiniki.mid" + }, + "Russian_Folk-Oy,_Da_Ne_Vecher": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 60, + "duration": [ + 105851852, + 79388889, + 63511111 + ], + "id": "Russian_Folk-Oy,_Da_Ne_Vecher", + "tags": [ + "folk" + ], + "tilesCount": [ + 213, + 220, + 221 + ], + "title": "Oy, Da Ne Vecher", + "url": "songs/folk/Russian_Folk-Oy,_Da_Ne_Vecher.mid" + }, + "Russian_Folk-Polyushka_Polye": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 120, + "duration": [ + 101777778, + 76333333, + 61066667 + ], + "id": "Russian_Folk-Polyushka_Polye", + "tags": [ + "folk" + ], + "tilesCount": [ + 221, + 222, + 222 + ], + "title": "Polyushka Polye", + "url": "songs/folk/Russian_Folk-Polyushka_Polye.mid" + }, + "Russian_Folk-Smuglyanka": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 120, + "duration": [ + 258666667, + 194000000, + 155200000 + ], + "id": "Russian_Folk-Smuglyanka", + "tags": [ + "folk" + ], + "tilesCount": [ + 799, + 1012, + 1117 + ], + "title": "Smuglyanka", + "url": "songs/folk/Russian_Folk-Smuglyanka.mid" + }, + "Russian_Folk-Troika": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 69, + "duration": [ + 855966184, + 641974638, + 513579710 + ], + "id": "Russian_Folk-Troika", + "tags": [ + "folk" + ], + "tilesCount": [ + 1174, + 1318, + 1337 + ], + "title": "Troika", + "url": "songs/folk/Russian_Folk-Troika.mid" + }, + "Russian_Folk-Tumbalalaika": { + "__collections__": {}, + "artist": "Russian Folk", + "bpm": 180, + "duration": [ + 188148148, + 141111111, + 112888889 + ], + "id": "Russian_Folk-Tumbalalaika", + "tags": [ + "folk" + ], + "tilesCount": [ + 396, + 523, + 543 + ], + "title": "Tumbalalaika", + "url": "songs/folk/Russian_Folk-Tumbalalaika.mid" + }, + "SHINee-Hello": { + "__collections__": {}, + "artist": "SHINee", + "bpm": 120, + "duration": [ + 296000000, + 222000000, + 177600000 + ], + "id": "SHINee-Hello", + "tags": [ + "kpop" + ], + "tilesCount": [ + 1120, + 1229, + 1298 + ], + "title": "Hello", + "url": "songs/kpop/SHINee-Hello.mid" + }, + "Saint_Saens-Carnival_Of_The_Animals_-_Aquarium": { + "__collections__": {}, + "artist": "Saint Saens", + "bpm": 37, + "duration": [ + 359729730, + 269797297, + 215837838 + ], + "id": "Saint_Saens-Carnival_Of_The_Animals_-_Aquarium", + "tags": [ + "classic" + ], + "tilesCount": [ + 1698, + 1744, + 1759 + ], + "title": "Carnival Of The Animals - Aquarium", + "url": "songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Aquarium.mid" + }, + "Saint_Saens-Carnival_Of_The_Animals_-_Finale": { + "__collections__": {}, + "artist": "Saint Saens", + "bpm": 74, + "duration": [ + 440090090, + 330067568, + 264054054 + ], + "id": "Saint_Saens-Carnival_Of_The_Animals_-_Finale", + "tags": [ + "classic" + ], + "tilesCount": [ + 1902, + 2395, + 2756 + ], + "title": "Carnival Of The Animals - Finale", + "url": "songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Finale.mid" + }, + "Saint_Saens-Danse_Macabre": { + "__collections__": {}, + "artist": "Saint Saens", + "bpm": 149, + "duration": [ + 216733781, + 162550336, + 130040268 + ], + "id": "Saint_Saens-Danse_Macabre", + "tags": [ + "classic" + ], + "tilesCount": [ + 1020, + 1360, + 1622 + ], + "title": "Danse Macabre", + "url": "songs/classic/Saint_Saens-Danse_Macabre.mid" + }, + "Saint_Saens-Symphony_No._3": { + "__collections__": {}, + "artist": "Saint Saens", + "bpm": 75, + "duration": [ + 204800000, + 153600000, + 122880000 + ], + "id": "Saint_Saens-Symphony_No._3", + "tags": [ + "classic" + ], + "tilesCount": [ + 482, + 632, + 769 + ], + "title": "Symphony No. 3", + "url": "songs/classic/Saint_Saens-Symphony_No._3.mid" + }, + "Samuel_Barber-Adagio_For_Strings": { + "__collections__": {}, + "artist": "Samuel Barber", + "bpm": 83, + "duration": [ + 542329317, + 406746988, + 325397590 + ], + "id": "Samuel_Barber-Adagio_For_Strings", + "tags": [ + "classic" + ], + "tilesCount": [ + 597, + 693, + 750 + ], + "title": "Adagio For Strings", + "url": "songs/classic/Samuel_Barber-Adagio_For_Strings.mid" + }, + "Schubert-Ave_Maria": { + "__collections__": {}, + "artist": "Schubert", + "bpm": 37, + "duration": [ + 258903904, + 194177928, + 155342342 + ], + "id": "Schubert-Ave_Maria", + "tags": [ + "classic" + ], + "tilesCount": [ + 1070, + 1073, + 1074 + ], + "title": "Ave Maria", + "url": "songs/classic/Schubert-Ave_Maria.mid" + }, + "Scott_Joplin-Maple_Leaf_Rag": { + "__collections__": {}, + "artist": "Scott Joplin", + "bpm": 74, + "duration": [ + 311261261, + 233445946, + 186756757 + ], + "id": "Scott_Joplin-Maple_Leaf_Rag", + "tags": [ + "other" + ], + "tilesCount": [ + 1703, + 2121, + 2409 + ], + "title": "Maple Leaf Rag", + "url": "songs/other/Scott_Joplin-Maple_Leaf_Rag.mid" + }, + "Shakira-Waka_Waka_(This_Time_For_Africa)": { + "__collections__": {}, + "artist": "Shakira", + "bpm": 75, + "duration": [ + 410607407, + 307955556, + 246364444 + ], + "id": "Shakira-Waka_Waka_(This_Time_For_Africa)", + "tags": [ + "pop" + ], + "tilesCount": [ + 1632, + 1796, + 1820 + ], + "title": "Waka Waka (This Time For Africa)", + "url": "songs/pop/Shakira-Waka_Waka_(This_Time_For_Africa).mid" + }, + "Shostakovich-Waltz_No._2": { + "__collections__": {}, + "artist": "Shostakovich", + "bpm": 169, + "duration": [ + 533648915, + 400236686, + 320189349 + ], + "id": "Shostakovich-Waltz_No._2", + "tags": [ + "other" + ], + "tilesCount": [ + 1415, + 1934, + 2236 + ], + "title": "Waltz No. 2", + "url": "songs/other/Shostakovich-Waltz_No._2.mid" + }, + "Smetana-Moldau_River": { + "__collections__": {}, + "artist": "Smetana", + "bpm": 120, + "duration": [ + 151111111, + 113333333, + 90666667 + ], + "id": "Smetana-Moldau_River", + "tags": [ + "classic" + ], + "tilesCount": [ + 559, + 711, + 805 + ], + "title": "Moldau River", + "url": "songs/classic/Smetana-Moldau_River.mid" + }, + "Stravinsky-Firebird_Suite_-_Finale": { + "__collections__": {}, + "artist": "Stravinsky", + "bpm": 75, + "duration": [ + 486844444, + 365133333, + 292106667 + ], + "id": "Stravinsky-Firebird_Suite_-_Finale", + "tags": [ + "classic" + ], + "tilesCount": [ + 1520, + 1708, + 1794 + ], + "title": "Firebird Suite - Finale", + "url": "songs/classic/Stravinsky-Firebird_Suite_-_Finale.mid" + }, + "Takahiro_Obata-Isabella's_Lullaby": { + "__collections__": {}, + "artist": "Takahiro Obata", + "bpm": 112, + "duration": [ + 198893849, + 149170387, + 119336310 + ], + "id": "Takahiro_Obata-Isabella's_Lullaby", + "tags": [ + "other" + ], + "tilesCount": [ + 502, + 615, + 656 + ], + "title": "Isabella's Lullaby", + "url": "songs/other/Takahiro_Obata-Isabella's_Lullaby.mid" + }, + "Tchaikovsky-Dance_Of_The_Little_Swans": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 100, + "duration": [ + 203200000, + 152400000, + 121920000 + ], + "id": "Tchaikovsky-Dance_Of_The_Little_Swans", + "tags": [ + "classic" + ], + "tilesCount": [ + 598, + 763, + 810 + ], + "title": "Dance Of The Little Swans", + "url": "songs/classic/Tchaikovsky-Dance_Of_The_Little_Swans.mid" + }, + "Tchaikovsky-Dance_Of_The_Reed_Flutes": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 69, + "duration": [ + 206932367, + 155199275, + 124159420 + ], + "id": "Tchaikovsky-Dance_Of_The_Reed_Flutes", + "tags": [ + "classic" + ], + "tilesCount": [ + 993, + 1288, + 1416 + ], + "title": "Dance Of The Reed Flutes", + "url": "songs/classic/Tchaikovsky-Dance_Of_The_Reed_Flutes.mid" + }, + "Tchaikovsky-Dance_Of_The_Sugar_Plum_Fairy": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 18, + "duration": [ + 469004630, + 351753472, + 281402778 + ], + "id": "Tchaikovsky-Dance_Of_The_Sugar_Plum_Fairy", + "tags": [ + "classic" + ], + "tilesCount": [ + 727, + 914, + 1018 + ], + "title": "Dance Of The Sugar Plum Fairy", + "url": "songs/classic/Tchaikovsky-Dance_Of_The_Sugar_Plum_Fairy.mid" + }, + "Tchaikovsky-Marche_Slave": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 72, + "duration": [ + 220000000, + 165000000, + 132000000 + ], + "id": "Tchaikovsky-Marche_Slave", + "tags": [ + "classic" + ], + "tilesCount": [ + 383, + 464, + 470 + ], + "title": "Marche Slave", + "url": "songs/classic/Tchaikovsky-Marche_Slave.mid" + }, + "Tchaikovsky-Nutcracker_-_March": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 75, + "duration": [ + 435881481, + 326911111, + 261528889 + ], + "id": "Tchaikovsky-Nutcracker_-_March", + "tags": [ + "classic" + ], + "tilesCount": [ + 1675, + 2061, + 2345 + ], + "title": "Nutcracker - March", + "url": "songs/classic/Tchaikovsky-Nutcracker_-_March.mid" + }, + "Tchaikovsky-Piano_Concerto_No._1": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 120, + "duration": [ + 419648148, + 314736111, + 251788889 + ], + "id": "Tchaikovsky-Piano_Concerto_No._1", + "tags": [ + "classic" + ], + "tilesCount": [ + 1007, + 1258, + 1369 + ], + "title": "Piano Concerto No. 1", + "url": "songs/classic/Tchaikovsky-Piano_Concerto_No._1.mid" + }, + "Tchaikovsky-Romeo_And_Juliet_Fantasy_Overture": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 37, + "duration": [ + 956929429, + 717697072, + 574157658 + ], + "id": "Tchaikovsky-Romeo_And_Juliet_Fantasy_Overture", + "tags": [ + "classic" + ], + "tilesCount": [ + 1227, + 1411, + 1558 + ], + "title": "Romeo And Juliet Fantasy Overture", + "url": "songs/classic/Tchaikovsky-Romeo_And_Juliet_Fantasy_Overture.mid" + }, + "Tchaikovsky-Sleeping_Beauty": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 150, + "duration": [ + 477688889, + 358266667, + 286613333 + ], + "id": "Tchaikovsky-Sleeping_Beauty", + "tags": [ + "classic" + ], + "tilesCount": [ + 2110, + 2670, + 2918 + ], + "title": "Sleeping Beauty", + "url": "songs/classic/Tchaikovsky-Sleeping_Beauty.mid" + }, + "Tchaikovsky-Swan_Lake": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 100, + "duration": [ + 86133333, + 64600000, + 51680000 + ], + "id": "Tchaikovsky-Swan_Lake", + "tags": [ + "classic" + ], + "tilesCount": [ + 326, + 326, + 326 + ], + "title": "Swan Lake", + "url": "songs/classic/Tchaikovsky-Swan_Lake.mid" + }, + "Tchaikovsky-Symphony_No._6": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 143, + "duration": [ + 617435897, + 463076923, + 370461538 + ], + "id": "Tchaikovsky-Symphony_No._6", + "tags": [ + "classic" + ], + "tilesCount": [ + 512, + 620, + 683 + ], + "title": "Symphony No. 6", + "url": "songs/classic/Tchaikovsky-Symphony_No._6.mid" + }, + "Tchaikovsky-Waltz_Of_The_Flowers": { + "__collections__": {}, + "artist": "Tchaikovsky", + "bpm": 182, + "duration": [ + 389597070, + 292197802, + 233758242 + ], + "id": "Tchaikovsky-Waltz_Of_The_Flowers", + "tags": [ + "classic" + ], + "tilesCount": [ + 602, + 784, + 889 + ], + "title": "Waltz Of The Flowers", + "url": "songs/classic/Tchaikovsky-Waltz_Of_The_Flowers.mid" + }, + "The_Scientist-Coldplay": { + "__collections__": {}, + "artist": "The Scientist", + "bpm": 75, + "duration": [ + 402311111, + 301733333, + 241386667 + ], + "id": "The_Scientist-Coldplay", + "tags": [ + "other" + ], + "tilesCount": [ + 949, + 1283, + 1460 + ], + "title": "Coldplay", + "url": "songs/other/The_Scientist-Coldplay.mid" + }, + "Tokyo_Ghoul-Unravel": { + "__collections__": {}, + "artist": "Tokyo Ghoul", + "bpm": 75, + "duration": [ + 614366667, + 460775000, + 368620000 + ], + "id": "Tokyo_Ghoul-Unravel", + "tags": [ + "other" + ], + "tilesCount": [ + 2595, + 3016, + 3243 + ], + "title": "Unravel", + "url": "songs/other/Tokyo_Ghoul-Unravel.mid" + }, + "Vaughan_Williams-Fantasia_On_Greensleeves": { + "__collections__": {}, + "artist": "Vaughan Williams", + "bpm": 120, + "duration": [ + 341555556, + 256166667, + 204933333 + ], + "id": "Vaughan_Williams-Fantasia_On_Greensleeves", + "tags": [ + "classic" + ], + "tilesCount": [ + 791, + 911, + 948 + ], + "title": "Fantasia On Greensleeves", + "url": "songs/classic/Vaughan_Williams-Fantasia_On_Greensleeves.mid" + }, + "Vietnamese_Folk-A_Test_Song": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 80, + "duration": [ + 1074074, + 805556, + 644444 + ], + "id": "Vietnamese_Folk-A_Test_Song", + "tags": [ + "songs" + ], + "tilesCount": [ + 2, + 2, + 2 + ], + "title": "A Test Song", + "url": "songs/Vietnamese_Folk-A_Test_Song.mid" + }, + "Vietnamese_Folk-Bac_Kim_Thang": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 120, + "duration": [ + 127555556, + 95666667, + 76533333 + ], + "id": "Vietnamese_Folk-Bac_Kim_Thang", + "tags": [ + "folk" + ], + "tilesCount": [ + 148, + 148, + 148 + ], + "title": "Bac Kim Thang", + "url": "songs/folk/Vietnamese_Folk-Bac_Kim_Thang.mid" + }, + "Vietnamese_Folk-Beo_Dat_May_Troi": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 120, + "duration": [ + 265666667, + 199250000, + 159400000 + ], + "id": "Vietnamese_Folk-Beo_Dat_May_Troi", + "tags": [ + "folk" + ], + "tilesCount": [ + 741, + 751, + 759 + ], + "title": "Beo Dat May Troi", + "url": "songs/folk/Vietnamese_Folk-Beo_Dat_May_Troi.mid" + }, + "Vietnamese_Folk-Cay_Truc_Xinh": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 60, + "duration": [ + 65333333, + 49000000, + 39200000 + ], + "id": "Vietnamese_Folk-Cay_Truc_Xinh", + "tags": [ + "folk" + ], + "tilesCount": [ + 54, + 54, + 54 + ], + "title": "Cay Truc Xinh", + "url": "songs/folk/Vietnamese_Folk-Cay_Truc_Xinh.mid" + }, + "Vietnamese_Folk-Co_La": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 60, + "duration": [ + 80000000, + 60000000, + 48000000 + ], + "id": "Vietnamese_Folk-Co_La", + "tags": [ + "folk" + ], + "tilesCount": [ + 59, + 59, + 59 + ], + "title": "Co La", + "url": "songs/folk/Vietnamese_Folk-Co_La.mid" + }, + "Vietnamese_Folk-Ly_Cay_Bong": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 120, + "duration": [ + 88888889, + 66666667, + 53333333 + ], + "id": "Vietnamese_Folk-Ly_Cay_Bong", + "tags": [ + "folk" + ], + "tilesCount": [ + 58, + 58, + 58 + ], + "title": "Ly Cay Bong", + "url": "songs/folk/Vietnamese_Folk-Ly_Cay_Bong.mid" + }, + "Vietnamese_Folk-Nguoi_O_Dung_Ve": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 60, + "duration": [ + 117796296, + 88347222, + 70677778 + ], + "id": "Vietnamese_Folk-Nguoi_O_Dung_Ve", + "tags": [ + "folk" + ], + "tilesCount": [ + 159, + 159, + 159 + ], + "title": "Nguoi O Dung Ve", + "url": "songs/folk/Vietnamese_Folk-Nguoi_O_Dung_Ve.mid" + }, + "Vietnamese_Folk-Trong_Com": { + "__collections__": {}, + "artist": "Vietnamese Folk", + "bpm": 60, + "duration": [ + 154666667, + 116000000, + 92800000 + ], + "id": "Vietnamese_Folk-Trong_Com", + "tags": [ + "folk" + ], + "tilesCount": [ + 233, + 233, + 233 + ], + "title": "Trong Com", + "url": "songs/folk/Vietnamese_Folk-Trong_Com.mid" + }, + "Vince_Guaraldi-Linus_And_Lucy": { + "__collections__": {}, + "artist": "Vince Guaraldi", + "bpm": 150, + "duration": [ + 239792593, + 179844444, + 143875556 + ], + "id": "Vince_Guaraldi-Linus_And_Lucy", + "tags": [ + "other" + ], + "tilesCount": [ + 1050, + 1242, + 1282 + ], + "title": "Linus And Lucy", + "url": "songs/other/Vince_Guaraldi-Linus_And_Lucy.mid" + }, + "Vittorio_Monti-Czardas": { + "__collections__": {}, + "artist": "Vittorio Monti", + "bpm": 55, + "duration": [ + 685010101, + 513757576, + 411006061 + ], + "id": "Vittorio_Monti-Czardas", + "tags": [ + "classic" + ], + "tilesCount": [ + 1849, + 2152, + 2342 + ], + "title": "Czardas", + "url": "songs/classic/Vittorio_Monti-Czardas.mid" + }, + "Vivaldi-Four_Seasons_-_Spring": { + "__collections__": {}, + "artist": "Vivaldi", + "bpm": 37, + "duration": [ + 682072072, + 511554054, + 409243243 + ], + "id": "Vivaldi-Four_Seasons_-_Spring", + "tags": [ + "classic" + ], + "tilesCount": [ + 2023, + 2249, + 2419 + ], + "title": "Four Seasons - Spring", + "url": "songs/classic/Vivaldi-Four_Seasons_-_Spring.mid" + }, + "Vivaldi-Four_Seasons_-_Summer": { + "__collections__": {}, + "artist": "Vivaldi", + "bpm": 75, + "duration": [ + 406844444, + 305133333, + 244106667 + ], + "id": "Vivaldi-Four_Seasons_-_Summer", + "tags": [ + "classic" + ], + "tilesCount": [ + 2362, + 2548, + 2553 + ], + "title": "Four Seasons - Summer", + "url": "songs/classic/Vivaldi-Four_Seasons_-_Summer.mid" + }, + "Vivaldi-Four_Seasons_-_Winter": { + "__collections__": {}, + "artist": "Vivaldi", + "bpm": 75, + "duration": [ + 305822222, + 229366667, + 183493333 + ], + "id": "Vivaldi-Four_Seasons_-_Winter", + "tags": [ + "classic" + ], + "tilesCount": [ + 1064, + 1187, + 1204 + ], + "title": "Four Seasons - Winter", + "url": "songs/classic/Vivaldi-Four_Seasons_-_Winter.mid" + }, + "Wham!-Last_Christmas-": { + "__collections__": {}, + "artist": "Wham!", + "bpm": 106, + "duration": [ + 340743711, + 255557783, + 204446226 + ], + "id": "Wham!-Last_Christmas-", + "tags": [ + "other" + ], + "tilesCount": [ + 1255, + 1366, + 1416 + ], + "title": "Last Christmas-", + "url": "songs/other/Wham!-Last_Christmas-.mid" + }, + "Yann_Tiersen-Comptine_Dune_Autre_Ete": { + "__collections__": {}, + "artist": "Yann Tiersen", + "bpm": 74, + "duration": [ + 399385886, + 299539414, + 239631532 + ], + "id": "Yann_Tiersen-Comptine_Dune_Autre_Ete", + "tags": [ + "other" + ], + "tilesCount": [ + 1722, + 1993, + 2036 + ], + "title": "Comptine Dune Autre Ete", + "url": "songs/other/Yann_Tiersen-Comptine_Dune_Autre_Ete.mid" + }, + "Yann_Tiersen-La_Valse_dAmelie": { + "__collections__": {}, + "artist": "Yann Tiersen", + "bpm": 100, + "duration": [ + 309600000, + 232200000, + 185760000 + ], + "id": "Yann_Tiersen-La_Valse_dAmelie", + "tags": [ + "other" + ], + "tilesCount": [ + 1039, + 1139, + 1164 + ], + "title": "La Valse dAmelie", + "url": "songs/other/Yann_Tiersen-La_Valse_dAmelie.mid" + }, + "Yiruma-Kiss_The_Rain": { + "__collections__": {}, + "artist": "Yiruma", + "bpm": 58, + "duration": [ + 533793103, + 400344828, + 320275862 + ], + "id": "Yiruma-Kiss_The_Rain", + "tags": [ + "other" + ], + "tilesCount": [ + 938, + 1069, + 1103 + ], + "title": "Kiss The Rain", + "url": "songs/other/Yiruma-Kiss_The_Rain.mid" + }, + "Yiruma-River_Flows_In_You": { + "__collections__": {}, + "artist": "Yiruma", + "bpm": 64, + "duration": [ + 240078125, + 180058594, + 144046875 + ], + "id": "Yiruma-River_Flows_In_You", + "tags": [ + "other" + ], + "tilesCount": [ + 824, + 839, + 840 + ], + "title": "River Flows In You", + "url": "songs/other/Yiruma-River_Flows_In_You.mid" + } + }, + "users": { + "0BeeRxhV6Vf5NXKrZq0lgkn79CS2": { + "__collections__": {}, + "id": "0BeeRxhV6Vf5NXKrZq0lgkn79CS2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "0EdvbPVXEzQ5BeJYShnIvuuYj8Q2": { + "__collections__": {}, + "id": "0EdvbPVXEzQ5BeJYShnIvuuYj8Q2", + "instrumentId": "piano", + "notificationTokens": [ + "cBVI8cziQ9G5puJevwcWBD:APA91bEi9nkH0zFZAo7Uc4O1VGcTPk1x4d8iGrT3Y80Up41JVjt6Xzj_Uo7n3SE2oHWRP2uDw1L5YP_G_lyAYLTHbm_mCC_ltb9k7-XFo7dmYCUBWyvPM9-4UpSq8O-s8cfjsUjWUThV" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "0ukyCEXiKCcCYyqZrpDcCNN3CbJ3": { + "__collections__": {}, + "id": "0ukyCEXiKCcCYyqZrpDcCNN3CbJ3", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "2XmAJsBkacSr6F2IXWF9BaUXDrh1": { + "__collections__": {}, + "id": "2XmAJsBkacSr6F2IXWF9BaUXDrh1", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "2qGH4o4qUaUze3uElDGMiF4SeiM2": { + "__collections__": {}, + "id": "2qGH4o4qUaUze3uElDGMiF4SeiM2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "43cIyyVCwSQYcq3j4a2p3PjzpoJ3": { + "__collections__": {}, + "id": "43cIyyVCwSQYcq3j4a2p3PjzpoJ3", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "7LS8zAa6KJPbgm06pAwGVgLCrrP2": { + "__collections__": {}, + "id": "7LS8zAa6KJPbgm06pAwGVgLCrrP2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "8xr2UR0zImfxs1tPtgjoJEMBko92": { + "__collections__": {}, + "id": "8xr2UR0zImfxs1tPtgjoJEMBko92", + "instrumentId": "piano", + "notificationTokens": [ + "cVA2tcziSzOhIzhYIfV4pf:APA91bHJ7oiUTwA3zrNn5_660XO6Wz2QTVBCasHPVXwEdnj993U3UlDx86StTqP0RnjkFWXgNf0r-3VPYDUTBzVZL8qmbYmSjcLIhrEawF0XfzuQaeSp8sDf2sDg4UJ_Fv-BtFSzTtKs" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "9biTHhe3NPe68NUryBPazmPP4sR2": { + "__collections__": {}, + "id": "9biTHhe3NPe68NUryBPazmPP4sR2", + "instrumentId": "piano", + "notificationTokens": [ + "fzfOU613SnK53Ss-GnZojg:APA91bGjWYrEs_LYd1Myj9R0Mky3cWdZWz4bdfrBws0ujnnkDu50iqV8PMEsYpzGUtFK3cRNO2UupYUcAZI1i_DxSPO-jSQ-gGc84dPyGVM3qQ1Fc-41t-Eeqjwyyfttrl0cX5qWOjyp" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "BLiLbBiWOEZOZnlBPnK2OFbUfou2": { + "__collections__": {}, + "id": "BLiLbBiWOEZOZnlBPnK2OFbUfou2", + "instrumentId": "piano", + "notificationTokens": [ + "cSvkk9y0RkuGQYoJYtW9bD:APA91bFQ58J2AnP7B-dq_AXpuDI0zgEssAq1YABpcfWArPA7xMiV96HocF3a9jGvutajM2zOHOIYorLg7sjKzpmxGYYGiax68i_rrdt29SHysq5ZUvkzyzlT9n3DfGSsshAJEvTF3eGF" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "F59WYp7vBZQiQ9oiAsKTHg9yIaw1": { + "__collections__": {}, + "id": "F59WYp7vBZQiQ9oiAsKTHg9yIaw1", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "HKngqjDpyTdTDsbB4UmEeoiWDqN2": { + "__collections__": {}, + "id": "HKngqjDpyTdTDsbB4UmEeoiWDqN2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "Iii5bdHWs3ek6i70LGwiQ5PTzys2": { + "__collections__": {}, + "id": "Iii5bdHWs3ek6i70LGwiQ5PTzys2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "KW8wIcA0QhVaXV1WzDgn8xjv6GC3": { + "__collections__": {}, + "id": "KW8wIcA0QhVaXV1WzDgn8xjv6GC3", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "LUhrzNDdKXPMvnMLYdxcgMudEIp1": { + "__collections__": {}, + "id": "LUhrzNDdKXPMvnMLYdxcgMudEIp1", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "QrpJSR0YUPRGAOrMgND5mmeyBQh1": { + "__collections__": {}, + "id": "QrpJSR0YUPRGAOrMgND5mmeyBQh1", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "R1VmLwjtC5dypJQIylIHnpIlXg62": { + "__collections__": {}, + "id": "R1VmLwjtC5dypJQIylIHnpIlXg62", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "RKGxgBzeHhOSWdI3sCRR3FoS45H2": { + "__collections__": {}, + "id": "RKGxgBzeHhOSWdI3sCRR3FoS45H2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "RhE7WvqGbCU7hkFNDuexQELJ8Id2": { + "__collections__": {}, + "id": "RhE7WvqGbCU7hkFNDuexQELJ8Id2", + "instrumentId": "piano", + "notificationTokens": [ + "cTdpYM-2QUCQJKboanScyq:APA91bGkXocGn-l036qhUHOKYd8A_Ozq5w98qEBwSidKnDpjYd3HXMk6NQ9ZBW8UtSDH99h1ugl64TLyjqMHB2bUihacXHKDnviCs-1U6_aKqZ-Eb_hMt2x3r1tJyZAr-jRIE_euzQTM" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "Rij0NF9i9cYmd1CG3ip9wUbLc3d2": { + "__collections__": {}, + "id": "Rij0NF9i9cYmd1CG3ip9wUbLc3d2", + "instrumentId": "piano", + "notificationTokens": [ + "f1XZ3E4hQceHYoLxhC0IQL:APA91bEYQz4UZuKGpZrz_rvW563wIZv90Pk8H_Lf4pok-k4G0CjUooPCqLVuTWBR2GgntmmeNQMAz777YqMU4asWX9467U7giTkfdFGg6RLR4_CbPq-GG4qxQcVXDgFNWSIYsVavdeA4", + "eRUVaaMGQ9GKLSsf_yslMB:APA91bG7ef5hatqqtOBI5_QYNcgbuarl6dtTLBzdAWeOrXFaAzs0igK3flrH2fku_2zlF6oqISChGbhjMfbZ63vbnPWfIVE_ZoJfySpJ9P4Y6G1zwwNtqxR7HBT9i23T4ycXm0zlfkQj" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "XvSB2o0VdIWugUUOlGvIxaHQSiv1": { + "__collections__": {}, + "id": "XvSB2o0VdIWugUUOlGvIxaHQSiv1", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "aKdLkus3mKRQaODMcKzSuJgowT72": { + "__collections__": {}, + "id": "aKdLkus3mKRQaODMcKzSuJgowT72", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "amgaWgNeWSYqSM6SNzpOfc5Nkyz2": { + "__collections__": {}, + "id": "amgaWgNeWSYqSM6SNzpOfc5Nkyz2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "gIRFROQsoGQzyMxA6XRU9X3L89D2": { + "__collections__": {}, + "id": "gIRFROQsoGQzyMxA6XRU9X3L89D2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "jXc2Vn4VNshi2eINGJG3I8c2iV43": { + "__collections__": {}, + "id": "jXc2Vn4VNshi2eINGJG3I8c2iV43", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "lm0XcaXECOXOEPJhTLSQTBz7weY2": { + "__collections__": {}, + "id": "lm0XcaXECOXOEPJhTLSQTBz7weY2", + "instrumentId": "piano", + "notificationTokens": [ + "daAm4JyMQs2a25AukiVRdd:APA91bEsNWE0VQmrNLZmGqZcXBHk8uTyvK8F0-d_mijAv8NVAvbyoDEMj2ZZA0ptvYFRRHlqm-kjnphgCkFIaZTuFw7gmmvBLcvtMPuyN_U0xklgInCtQT5PY7fs3k-kPGHdqNkGdb1R" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "lpxDXty32iXKLKWJ1BOn1jmaEaN2": { + "__collections__": {}, + "id": "lpxDXty32iXKLKWJ1BOn1jmaEaN2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "nVVup1IcJBNtkrPjIWnOgo3t77v2": { + "__collections__": {}, + "id": "nVVup1IcJBNtkrPjIWnOgo3t77v2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "o0iXwocdbLeLmxscbf2jSpEWOUH3": { + "__collections__": {}, + "id": "o0iXwocdbLeLmxscbf2jSpEWOUH3", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "oSF8ug2viyNgIieFNrCDHdacYTR2": { + "__collections__": {}, + "id": "oSF8ug2viyNgIieFNrCDHdacYTR2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "put8pZHQ8RfpVo1bDoyW7iyk9Xo2": { + "__collections__": {}, + "id": "put8pZHQ8RfpVo1bDoyW7iyk9Xo2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "s1aSboU0bEfvA4CReOTkKhY0cmF3": { + "__collections__": {}, + "id": "s1aSboU0bEfvA4CReOTkKhY0cmF3", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "saxyGwgvMsS1FhnImc0lHWXo6h33": { + "__collections__": {}, + "id": "saxyGwgvMsS1FhnImc0lHWXo6h33", + "instrumentId": "piano", + "notificationTokens": [ + "f5BBRlgjTt2WftUdnSdMqW:APA91bFZZNRYjODOVAY2XJ9VaXJDHxCbxo1s6tpG2rSOYdt-jO98864mlMrOl7apSsMkZ3quBwGtxwuSfXd_Gn3TG6Wfz_T2O39gPdh8mywFv187_QEqjwZ9UBlUw-B_NFHsf09v7xMg" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "vNpikramyRU7UnCGkZfc956Cr9v1": { + "__collections__": {}, + "id": "vNpikramyRU7UnCGkZfc956Cr9v1", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "ygnHbB5EwthSbdGhllKQeW4YlXM2": { + "__collections__": {}, + "id": "ygnHbB5EwthSbdGhllKQeW4YlXM2", + "instrumentId": "piano", + "notificationTokens": [], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + }, + "ymeeqn0mHHZkWmyk1mvRuWjj1J32": { + "__collections__": {}, + "id": "ymeeqn0mHHZkWmyk1mvRuWjj1J32", + "instrumentId": "piano", + "notificationTokens": [ + "eD7WBAKnQ02anvf7jNd8uq:APA91bHGGbtw33ubbGT-AHuBKr8XliH0ITrcyd47g1zUk9cX78eo3eYPVclIb35Bz7EiVANV16xu2W-Syrf6nw3YrGl4axyOwerJ5mdApMwQLAhkRT4U0hRuX-jNLiQimGcaaRXw1OlM" + ], + "playedNotes": 0, + "playedTime": 0, + "stars": 0 + } + } + } +} diff --git a/database/jq-win64.exe b/database/jq-win64.exe new file mode 100644 index 00000000..cf9f37e4 Binary files /dev/null and b/database/jq-win64.exe differ diff --git a/database/restore-firestore.sh b/database/restore-firestore.sh new file mode 100644 index 00000000..cb51600f --- /dev/null +++ b/database/restore-firestore.sh @@ -0,0 +1,4 @@ +# curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - +# sudo apt-get install -y nodejs +# npm install -g node-firestore-import-export; +firestore-import --backupFile db.json --yes \ No newline at end of file diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 00000000..9c329c63 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file("service_account.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +package_name("com.chaomao.hittick") diff --git a/fastlane/metadata/en-US/changelogs/19.txt b/fastlane/metadata/en-US/changelogs/19.txt new file mode 100644 index 00000000..86309c81 --- /dev/null +++ b/fastlane/metadata/en-US/changelogs/19.txt @@ -0,0 +1 @@ +Fix bugs and improve performance \ No newline at end of file diff --git a/fastlane/metadata/en-US/full_description.txt b/fastlane/metadata/en-US/full_description.txt new file mode 100644 index 00000000..d6f24b18 --- /dev/null +++ b/fastlane/metadata/en-US/full_description.txt @@ -0,0 +1,11 @@ +Even you don't have any basic knowledge of music instruments, you still can play with falling notes in the game! + +* Hundreds of songs, from classics of Beethoven, Chopin, Mozart, or Schubert, folk songs to POP/EDM. Will be updated continuously +* Adjustable difficulty level +* Adjustable tempo +* Many game modes +* Many musical instruments +* Play offline without the internet +* Upload your song and play your way + +Just select your favorites songs, hit music notes, and listen. \ No newline at end of file diff --git a/fastlane/metadata/en-US/images/featureGraphic.png b/fastlane/metadata/en-US/images/featureGraphic.png new file mode 100644 index 00000000..10b4904b Binary files /dev/null and b/fastlane/metadata/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/en-US/images/icon.png b/fastlane/metadata/en-US/images/icon.png new file mode 100644 index 00000000..6c4f8257 Binary files /dev/null and b/fastlane/metadata/en-US/images/icon.png differ diff --git a/fastlane/metadata/en-US/images/phoneScreenshots/1_en-US.png b/fastlane/metadata/en-US/images/phoneScreenshots/1_en-US.png new file mode 100644 index 00000000..df96c326 Binary files /dev/null and b/fastlane/metadata/en-US/images/phoneScreenshots/1_en-US.png differ diff --git a/fastlane/metadata/en-US/images/phoneScreenshots/2_en-US.png b/fastlane/metadata/en-US/images/phoneScreenshots/2_en-US.png new file mode 100644 index 00000000..3740d0fa Binary files /dev/null and b/fastlane/metadata/en-US/images/phoneScreenshots/2_en-US.png differ diff --git a/fastlane/metadata/en-US/images/phoneScreenshots/3_en-US.png b/fastlane/metadata/en-US/images/phoneScreenshots/3_en-US.png new file mode 100644 index 00000000..6808e243 Binary files /dev/null and b/fastlane/metadata/en-US/images/phoneScreenshots/3_en-US.png differ diff --git a/fastlane/metadata/en-US/short_description.txt b/fastlane/metadata/en-US/short_description.txt new file mode 100644 index 00000000..bf0f9fa6 --- /dev/null +++ b/fastlane/metadata/en-US/short_description.txt @@ -0,0 +1 @@ +Play Piano, Guitar and more. Hit falling notes to play hundreds of songs easily. \ No newline at end of file diff --git a/fastlane/metadata/en-US/title.txt b/fastlane/metadata/en-US/title.txt new file mode 100644 index 00000000..7e05b5c2 --- /dev/null +++ b/fastlane/metadata/en-US/title.txt @@ -0,0 +1 @@ +Hit Notes - Play instruments \ No newline at end of file diff --git a/fastlane/metadata/en-US/video.txt b/fastlane/metadata/en-US/video.txt new file mode 100644 index 00000000..1e634847 --- /dev/null +++ b/fastlane/metadata/en-US/video.txt @@ -0,0 +1 @@ +https://www.youtube.com/watch?v=dzI4UCgvdQc \ No newline at end of file diff --git a/fastlane/metadata/vi/changelogs/19.txt b/fastlane/metadata/vi/changelogs/19.txt new file mode 100644 index 00000000..11b4ee78 --- /dev/null +++ b/fastlane/metadata/vi/changelogs/19.txt @@ -0,0 +1 @@ +Sửa lỗi và cải thiện hiệu suất \ No newline at end of file diff --git a/fastlane/metadata/vi/full_description.txt b/fastlane/metadata/vi/full_description.txt new file mode 100644 index 00000000..549d34b3 --- /dev/null +++ b/fastlane/metadata/vi/full_description.txt @@ -0,0 +1,11 @@ +Ngay cả khi bạn không có bất kỳ kiến thức cơ bản nào về nhạc cụ, bạn vẫn có thể chơi với các nốt nhạc đang rơi trong trò chơi! + +* Hàng trăm bản nhạc, từ các tác phẩm kinh điển của Beethoven, Chopin, Mozart hoặc Schubert, các bản nhạc dân gian đến POP / EDM. Sẽ được cập nhật liên tục +* Mức độ khó có thể điều chỉnh +* Nhịp độ có thể điều chỉnh +* Nhiều chế độ chơi +* Nhiều nhạc cụ +* Chơi ngoại tuyến mà không cần kết nối mạng +* Tải lên bản nhạc của bạn và chơi theo cách của bạn + +Chỉ cần chọn các bản nhạc yêu thích của bạn, nhấn các nốt nhạc và lắng nghe. \ No newline at end of file diff --git a/fastlane/metadata/vi/images/featureGraphic.png b/fastlane/metadata/vi/images/featureGraphic.png new file mode 100644 index 00000000..b9a0ed05 Binary files /dev/null and b/fastlane/metadata/vi/images/featureGraphic.png differ diff --git a/fastlane/metadata/vi/images/icon.png b/fastlane/metadata/vi/images/icon.png new file mode 100644 index 00000000..6c4f8257 Binary files /dev/null and b/fastlane/metadata/vi/images/icon.png differ diff --git a/fastlane/metadata/vi/images/phoneScreenshots/1_vi.png b/fastlane/metadata/vi/images/phoneScreenshots/1_vi.png new file mode 100644 index 00000000..69ac1070 Binary files /dev/null and b/fastlane/metadata/vi/images/phoneScreenshots/1_vi.png differ diff --git a/fastlane/metadata/vi/images/phoneScreenshots/2_vi.png b/fastlane/metadata/vi/images/phoneScreenshots/2_vi.png new file mode 100644 index 00000000..4abf5747 Binary files /dev/null and b/fastlane/metadata/vi/images/phoneScreenshots/2_vi.png differ diff --git a/fastlane/metadata/vi/images/phoneScreenshots/3_vi.png b/fastlane/metadata/vi/images/phoneScreenshots/3_vi.png new file mode 100644 index 00000000..56ce38e6 Binary files /dev/null and b/fastlane/metadata/vi/images/phoneScreenshots/3_vi.png differ diff --git a/fastlane/metadata/vi/short_description.txt b/fastlane/metadata/vi/short_description.txt new file mode 100644 index 00000000..b3995df8 --- /dev/null +++ b/fastlane/metadata/vi/short_description.txt @@ -0,0 +1 @@ +Chơi Piano, Guitar và hơn nữa. Chạm vào các nốt để chơi hàng trăm bản nhạc. \ No newline at end of file diff --git a/fastlane/metadata/vi/title.txt b/fastlane/metadata/vi/title.txt new file mode 100644 index 00000000..f464e663 --- /dev/null +++ b/fastlane/metadata/vi/title.txt @@ -0,0 +1 @@ +Hit Notes - Piano Guitar Việt \ No newline at end of file diff --git a/fastlane/metadata/vi/video.txt b/fastlane/metadata/vi/video.txt new file mode 100644 index 00000000..1e634847 --- /dev/null +++ b/fastlane/metadata/vi/video.txt @@ -0,0 +1 @@ +https://www.youtube.com/watch?v=dzI4UCgvdQc \ No newline at end of file diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 00000000..24b8f589 --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1,9 @@ +.firebaserc +/.vs/ +/.vscode/ +/functions/lib/ +*.log +/functions/node_modules/ + +## Google api key to access play console +/functions/service_account.json diff --git a/functions/firebase.json b/functions/firebase.json new file mode 100644 index 00000000..8a2d399e --- /dev/null +++ b/functions/firebase.json @@ -0,0 +1,16 @@ +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "functions": { + "predeploy": [ + "npm --prefix ./functions run lint", + "npm --prefix ./functions run build" + ], + "source": "functions" + }, + "storage": { + "rules": "storage.rules" + } +} \ No newline at end of file diff --git a/functions/firestore.indexes.json b/functions/firestore.indexes.json new file mode 100644 index 00000000..dd713580 --- /dev/null +++ b/functions/firestore.indexes.json @@ -0,0 +1,19 @@ +{ + "indexes": [ + { + "collectionGroup": "songs", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "tags", + "arrayConfig": "CONTAINS" + }, + { + "fieldPath": "title", + "order": "ASCENDING" + } + ] + } + ], + "fieldOverrides": [] +} diff --git a/functions/firestore.rules b/functions/firestore.rules new file mode 100644 index 00000000..dd00fee7 --- /dev/null +++ b/functions/firestore.rules @@ -0,0 +1,38 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + function isSignedIn() { + return request.auth.uid != null; + } + function isItMine() { + return request.auth.uid == resource.data.id; + } + match /users/{userId} { + allow read: if isSignedIn(); + allow create: if false; + allow update: if isItMine() && isUpdatingInstrumentId(); + allow delete: if false; + function isUpdatingInstrumentId() { + return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['instrumentId', 'notificationTokens']); + } + } + match /songs/{songId} { + allow read: if isSignedIn(); + allow create: if false; + allow update: if false; + allow delete: if false; + } + match /instruments/{instrumentId} { + allow read: if isSignedIn(); + allow create: if false; + allow update: if false; + allow delete: if false; + } + match /games/{gameId} { + allow read: if false; + allow create: if request.auth.uid == gameId; + allow update: if false; + allow delete: if false; + } + } +} \ No newline at end of file diff --git a/functions/functions/package-lock.json b/functions/functions/package-lock.json new file mode 100644 index 00000000..05ab504a --- /dev/null +++ b/functions/functions/package-lock.json @@ -0,0 +1,2235 @@ +{ + "name": "functions", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "dev": true + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@fimbul/bifrost": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@fimbul/bifrost/-/bifrost-0.21.0.tgz", + "integrity": "sha512-ou8VU+nTmOW1jeg+FT+sn+an/M0Xb9G16RucrfhjXGWv1Q97kCoM5CG9Qj7GYOSdu7km72k7nY83Eyr53Bkakg==", + "dev": true, + "requires": { + "@fimbul/ymir": "^0.21.0", + "get-caller-file": "^2.0.0", + "tslib": "^1.8.1", + "tsutils": "^3.5.0" + }, + "dependencies": { + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@fimbul/ymir": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@fimbul/ymir/-/ymir-0.21.0.tgz", + "integrity": "sha512-T/y7WqPsm4n3zhT08EpB5sfdm2Kvw3gurAxr2Lr5dQeLi8ZsMlNT/Jby+ZmuuAAd1PnXYzKp+2SXgIkQIIMCUg==", + "dev": true, + "requires": { + "inversify": "^5.0.0", + "reflect-metadata": "^0.1.12", + "tslib": "^1.8.1" + } + }, + "@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" + }, + "@firebase/auth-interop-types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", + "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==" + }, + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@firebase/database": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz", + "integrity": "sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA==", + "requires": { + "@firebase/auth-interop-types": "0.1.5", + "@firebase/component": "0.1.19", + "@firebase/database-types": "0.5.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.2", + "faye-websocket": "0.11.3", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@firebase/database-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz", + "integrity": "sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g==", + "requires": { + "@firebase/app-types": "0.6.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" + }, + "@firebase/util": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", + "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", + "requires": { + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@google-cloud/common": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz", + "integrity": "sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==", + "optional": true, + "requires": { + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^6.1.1", + "retry-request": "^4.1.1", + "teeny-request": "^7.0.0" + } + }, + "@google-cloud/firestore": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.8.0.tgz", + "integrity": "sha512-cBPo7QQG+aUhS7AIr6fDlA9KIX0/U26rKZyL2K/L68LArDQzgBk1/xOiMoflHRNDQARwCQ0PAZmw8V8CXg7vTg==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^2.9.2" + } + }, + "@google-cloud/paginator": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz", + "integrity": "sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", + "optional": true + }, + "@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "optional": true + }, + "@google-cloud/storage": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.6.0.tgz", + "integrity": "sha512-nLcym8IuCzy1O7tNTXNFuMHfX900sTM3kSTqbKe7oFSoKUiaIM+FHuuuDimMMlieY6StA1xYNPRFFHz57Nv8YQ==", + "optional": true, + "requires": { + "@google-cloud/common": "^3.5.0", + "@google-cloud/paginator": "^3.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.0", + "compressible": "^2.0.12", + "date-and-time": "^0.14.0", + "duplexify": "^4.0.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "gcs-resumable-upload": "^3.1.0", + "get-stream": "^6.0.0", + "hash-stream-validation": "^0.2.2", + "mime": "^2.2.0", + "mime-types": "^2.0.8", + "onetime": "^5.1.0", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "snakeize": "^0.1.0", + "stream-events": "^1.0.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "requires": { + "yocto-queue": "^0.1.0" + } + } + } + }, + "@grpc/grpc-js": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.8.tgz", + "integrity": "sha512-64hg5rmEm6F/NvlWERhHmmgxbWU8nD2TMWE+9TvG7/WcOrFT3fzg/Uu631pXRFwmJ4aWO/kp9vVSlr8FUjBDLA==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.6.0-pre14", + "@types/node": "^12.12.47", + "google-auth-library": "^6.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "@grpc/proto-loader": { + "version": "0.6.0-pre9", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.0-pre9.tgz", + "integrity": "sha512-oM+LjpEjNzW5pNJjt4/hq1HYayNeQT+eGrOPABJnYHv7TyNPDNzkQ76rDYZF86X5swJOa4EujEMzQ9iiTdPgww==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.9.0", + "yargs": "^15.3.1" + } + }, + "@types/node": { + "version": "12.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.8.tgz", + "integrity": "sha512-D4k2kNi0URNBxIRCb1khTnkWNHv8KSL1owPmS/K5e5t8B2GzMReY7AsJIY1BnP5KdlgC4rj9jk2IkDMasIE7xg==", + "optional": true + } + } + }, + "@grpc/proto-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", + "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", + "optional": true, + "requires": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "optional": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "optional": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "optional": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "optional": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "optional": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "optional": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "optional": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "optional": true + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz", + "integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "optional": true + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/node": { + "version": "10.17.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.48.tgz", + "integrity": "sha512-Agl6xbYP6FOMDeAsr3QVZ+g7Yzg0uhPHWx0j5g4LFdUBHVtqtU+gH660k/lCEe506jJLOGbEzsnqPDTZGJQLag==" + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "requires": { + "debug": "4" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "optional": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "optional": true + }, + "bignumber.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==", + "optional": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "optional": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "optional": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "optional": true + }, + "date-and-time": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.1.tgz", + "integrity": "sha512-M4RggEH5OF2ZuCOxgOU67R6Z9ohjKbxGvAQz48vj53wLmL0bAgumkBvycR32f30pK+Og9pIR+RFDyChbaE4oLA==", + "optional": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "optional": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, + "doctrine": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", + "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", + "dev": true, + "requires": { + "esutils": "^1.1.6", + "isarray": "0.0.1" + }, + "dependencies": { + "esutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", + "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", + "dev": true + } + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "optional": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "optional": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "optional": true + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "optional": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "firebase-admin": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.4.1.tgz", + "integrity": "sha512-y9r2Mz2x1WTr60YrCDqz8Lw70DlwIvRIieVltP+UdRogkVpfnvyd+bi4D0KPlujW3teqcFPmxuzsXB+DP5vGfQ==", + "requires": { + "@firebase/database": "^0.6.10", + "@firebase/database-types": "^0.5.2", + "@google-cloud/firestore": "^4.5.0", + "@google-cloud/storage": "^5.3.0", + "@types/node": "^10.10.0", + "dicer": "^0.3.0", + "jsonwebtoken": "^8.5.1", + "node-forge": "^0.10.0" + } + }, + "firebase-functions": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.12.0.tgz", + "integrity": "sha512-C6XfFlnL9JPo2tRNpncxkXeXIpViLzKxBsfNTMB3M2i33mJFvkrLhxp9rhsCRSrcwxraBKmpeSvyh3FJkka54w==", + "requires": { + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "lodash": "^4.17.14" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "optional": true + }, + "gaxios": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.0.1.tgz", + "integrity": "sha512-jOin8xRZ/UytQeBpSXFqIzqU7Fi5TqgPNLlUsSB8kjJ76+FiGBfImF8KJu++c6J4jOldfJUtt0YmkRj2ZpSHTQ==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", + "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "gcs-resumable-upload": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz", + "integrity": "sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "configstore": "^5.0.0", + "extend": "^3.0.2", + "gaxios": "^3.0.0", + "google-auth-library": "^6.0.0", + "pumpify": "^2.0.0", + "stream-events": "^1.0.4" + }, + "dependencies": { + "gaxios": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", + "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + } + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "optional": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "google-auth-library": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.3.tgz", + "integrity": "sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-gax": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.9.2.tgz", + "integrity": "sha512-Pve4osEzNKpBZqFXMfGKBbKCtgnHpUe5IQMh5Ou+Xtg8nLcba94L3gF0xgM5phMdGRRqJn0SMjcuEVmOYu7EBg==", + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.1.1", + "@grpc/proto-loader": "^0.5.1", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^6.1.3", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "protobufjs": "^6.9.0", + "retry-request": "^4.0.0" + } + }, + "google-p12-pem": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", + "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "optional": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "optional": true + }, + "gtoken": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.1.0.tgz", + "integrity": "sha512-4d8N6Lk8TEAHl9vVoRVMh9BNOKWVgl2DdNtr3428O75r3QFrF/a5MMu851VmK0AA8+iSvbwRv69k5XnMLURGhg==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0", + "mime": "^2.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hash-stream-validation": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "optional": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "http-parser-js": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", + "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==" + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "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" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "optional": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inversify": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz", + "integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ==", + "dev": true + }, + "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==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "optional": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "optional": true + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "optional": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "optional": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "optional": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "requires": { + "semver": "^6.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "optional": true + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "optional": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + }, + "dependencies": { + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + } + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "optional": true + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "optional": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "optional": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "optional": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "optional": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "optional": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "protobufjs": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", + "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", + "optional": true, + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "13.13.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.34.tgz", + "integrity": "sha512-g8D1HF2dMDKYSDl5+79izRwRgNPsSynmWMbj50mj7GZ0b7Lv4p8EmZjbo3h0h+6iLr6YmVz9VnF6XVZ3O6V1Ug==", + "optional": true + } + } + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "optional": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "optional": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "retry-request": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", + "integrity": "sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==", + "optional": true, + "requires": { + "debug": "^4.1.1" + } + }, + "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==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "optional": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "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" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "optional": true + }, + "snakeize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", + "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", + "optional": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "optional": true + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "optional": true + }, + "teeny-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", + "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", + "optional": true, + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tslint": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.2.tgz", + "integrity": "sha512-UyNrLdK3E0fQG/xWNqAFAC5ugtFyPO4JJR1KyyfQAyzR8W0fTRrC91A8Wej4BntFzcvETdCSDa/4PnNYJQLYiA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.10.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "tslint-config-airbnb": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/tslint-config-airbnb/-/tslint-config-airbnb-5.11.2.tgz", + "integrity": "sha512-mUpHPTeeCFx8XARGG/kzYP4dPSOgoCqNiYbGHh09qTH8q+Y1ghsOgaeZKYYQT7IyxMos523z/QBaiv2zKNBcow==", + "dev": true, + "requires": { + "tslint-consistent-codestyle": "^1.14.1", + "tslint-eslint-rules": "^5.4.0", + "tslint-microsoft-contrib": "~5.2.1" + } + }, + "tslint-consistent-codestyle": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.16.0.tgz", + "integrity": "sha512-ebR/xHyMEuU36hGNOgCfjGBNYxBPixf0yU1Yoo6s3BrpBRFccjPOmIVaVvQsWAUAMdmfzHOCihVkcaMfimqvHw==", + "dev": true, + "requires": { + "@fimbul/bifrost": "^0.21.0", + "tslib": "^1.7.1", + "tsutils": "^2.29.0" + } + }, + "tslint-eslint-rules": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz", + "integrity": "sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w==", + "dev": true, + "requires": { + "doctrine": "0.7.2", + "tslib": "1.9.0", + "tsutils": "^3.0.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tslint-microsoft-contrib": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.2.1.tgz", + "integrity": "sha512-PDYjvpo0gN9IfMULwKk0KpVOPMhU6cNoT9VwCOLeDl/QS8v8W2yspRpFFuUS7/c5EIH/n8ApMi8TxJAz1tfFUA==", + "dev": true, + "requires": { + "tsutils": "^2.27.2 <2.29.0" + }, + "dependencies": { + "tsutils": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", + "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "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" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "optional": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz", + "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==", + "dev": true + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "optional": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "optional": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", + "optional": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "optional": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "optional": true + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "optional": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "optional": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "optional": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true + } + } +} diff --git a/functions/functions/package.json b/functions/functions/package.json new file mode 100644 index 00000000..bd7b0cfe --- /dev/null +++ b/functions/functions/package.json @@ -0,0 +1,27 @@ +{ + "name": "functions", + "scripts": { + "lint": "tslint --fix --project tsconfig.json", + "build": "tsc", + "serve": "npm run build && firebase serve --only functions", + "shell": "npm run build && firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log", + "test": "npm run build && mocha -r ts-node/register test/**/*.spec.ts --timeout 5000" + }, + "main": "lib/index.js", + "dependencies": { + "firebase-admin": "^9.4.1", + "firebase-functions": "^3.12.0" + }, + "devDependencies": { + "tslint": "^6.1.2", + "tslint-config-airbnb": "^5.11.2", + "typescript": "^4.1.2" + }, + "engines": { + "node": "10" + }, + "private": true +} diff --git a/functions/functions/src/firebase-path.ts b/functions/functions/src/firebase-path.ts new file mode 100644 index 00000000..41f67e0c --- /dev/null +++ b/functions/functions/src/firebase-path.ts @@ -0,0 +1,5 @@ +export class FirebasePath { + /* This file is synchronized with client code */ + public static readonly FIREBASE_PATH_USERS = 'users'; + public static readonly FIREBASE_PATH_SONGS = 'songs'; +} diff --git a/functions/functions/src/game/game-manager.ts b/functions/functions/src/game/game-manager.ts new file mode 100644 index 00000000..1e4b6f03 --- /dev/null +++ b/functions/functions/src/game/game-manager.ts @@ -0,0 +1,77 @@ +import { + HttpsError, + CallableContext, +} from 'firebase-functions/lib/providers/https'; +import { FirebasePath } from '../firebase-path'; +import * as firebase from 'firebase-admin'; +import { getUserId } from '../shared'; +import { Song } from '../model/song'; +import { GameReward } from '../model/game-reward'; +import { User } from '../model/user'; + +export async function getGameReward( + data: any, + context: CallableContext, +): Promise { + const songId = data.songId; + const difficulty = data.difficulty; + const speed = data.speed; + const errorCount = data.errorCount; + + const userId = getUserId(context); + console.log(`User ${userId} is getting game reward with songId ${songId}, difficulty ${difficulty}, speed ${speed}, errorCount ${errorCount}`); + const firestore = firebase.firestore(); + if ( + !( + Number.isSafeInteger(errorCount) && + errorCount >= 0 + ) + ) { + console.log('Invalid game information'); + throw new HttpsError('invalid-argument', 'Invalid game information'); + } + return firestore.runTransaction(async (transaction): Promise => { + const userRef = firestore + .collection(FirebasePath.FIREBASE_PATH_USERS) + .doc(userId); + const userSnapshot = await transaction.get(userRef); + if (!userSnapshot.exists) { + console.log('Cannot retrieve user information'); + throw new HttpsError('unavailable', 'Cannot retrieve user information'); + } + const user = userSnapshot.data() as User; + const songRef = firestore.collection(FirebasePath.FIREBASE_PATH_SONGS).doc(songId); + const songSnapshot = await transaction.get(songRef); + if (!songSnapshot.exists) { + console.log('Cannot retrieve played song information'); + throw new HttpsError( + 'unavailable', + 'Cannot retrieve played song information', + ); + } + const song: Song = songSnapshot.data() as Song; + + let stars = 0; + const tilesCount = song.tilesCount[difficulty]; + const errorPercent = errorCount / tilesCount; + if (errorPercent <= 0.05) { + stars = 3; + } else if (errorPercent <= 0.2) { + stars = 2; + } else { + stars = 1; + } + + user.playedNotes += tilesCount; + user.stars += tilesCount - errorCount; + user.playedTime += song.duration[speed]; + + transaction.set(userRef, user); + const reward: GameReward = { + stars, + playedNotes: tilesCount, + }; + console.log(`Reward of the user is ${JSON.stringify(reward)}`); + return reward; + }); +} diff --git a/functions/functions/src/index.ts b/functions/functions/src/index.ts new file mode 100644 index 00000000..fc46ddde --- /dev/null +++ b/functions/functions/src/index.ts @@ -0,0 +1,26 @@ +import * as functions from 'firebase-functions'; +import * as firebase from 'firebase-admin'; +import { onUserSignUp, onUserDeleted } from './user/user'; +import { getGameReward } from './game/game-manager'; + +const runtimeOpts: functions.RuntimeOptions = { + timeoutSeconds: 540, + memory: '128MB', +}; +firebase.initializeApp(); + +if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'onUserSignUp') { + exports.onUserSignUp = functions.runWith(runtimeOpts).auth + .user() + .onCreate(onUserSignUp); +} + +if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'onUserDeleted') { + exports.onUserDeleted = functions.runWith(runtimeOpts).auth + .user() + .onDelete(onUserDeleted); +} + +if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'getGameReward') { + exports.getGameReward = functions.runWith(runtimeOpts).https.onCall(getGameReward); +} diff --git a/functions/functions/src/model/game-reward.ts b/functions/functions/src/model/game-reward.ts new file mode 100644 index 00000000..8c9da9b3 --- /dev/null +++ b/functions/functions/src/model/game-reward.ts @@ -0,0 +1,4 @@ +export interface GameReward { + stars: number; + playedNotes: number; +} diff --git a/functions/functions/src/model/song.ts b/functions/functions/src/model/song.ts new file mode 100644 index 00000000..c48cf59d --- /dev/null +++ b/functions/functions/src/model/song.ts @@ -0,0 +1,9 @@ +export interface Song { + id: string; + title: string; + artist: string; + url: string; + bpm: number; + tilesCount: number[]; + duration: number[]; +} diff --git a/functions/functions/src/model/user.ts b/functions/functions/src/model/user.ts new file mode 100644 index 00000000..399bc445 --- /dev/null +++ b/functions/functions/src/model/user.ts @@ -0,0 +1,9 @@ + +export interface User { + id: string; + playedNotes: number; + stars: number; + playedTime: number; + instrumentId: string; + notificationTokens: string[]; +} diff --git a/functions/functions/src/shared.ts b/functions/functions/src/shared.ts new file mode 100644 index 00000000..6d5e0a29 --- /dev/null +++ b/functions/functions/src/shared.ts @@ -0,0 +1,14 @@ +import * as functions from 'firebase-functions'; +import { CallableContext } from 'firebase-functions/lib/providers/https'; + +export function getUserId(context: CallableContext): string { + if (!context.auth) { + console.log('Unauthorized Access'); + throw new functions.https.HttpsError( + 'unauthenticated', + 'Unauthorized Access', + ); + } else { + return context.auth.uid; + } +} diff --git a/functions/functions/src/user/user.ts b/functions/functions/src/user/user.ts new file mode 100644 index 00000000..32aa3566 --- /dev/null +++ b/functions/functions/src/user/user.ts @@ -0,0 +1,42 @@ +import * as firebase from 'firebase-admin'; +import { User } from '../model/user'; +import { FirebasePath } from '../firebase-path'; +import { EventContext } from 'firebase-functions'; +import UserRecord = firebase.auth.UserRecord; +import { HttpsError } from 'firebase-functions/lib/providers/https'; + +export function onUserSignUp(user: UserRecord, _: EventContext): Promise { + return firebase.firestore().runTransaction(async (transaction): Promise => { + const userRef = firebase + .firestore() + .collection(FirebasePath.FIREBASE_PATH_USERS) + .doc(user.uid); + const newUser: User = { + id: user.uid, + playedNotes: 0, + stars: 0, + playedTime: 0, + instrumentId: 'piano', + notificationTokens: [], + }; + transaction.set(userRef, newUser); + console.log(`Create profile success ${user.uid}`); + }); +} + +export function onUserDeleted(userRecord: UserRecord): Promise { + return firebase.firestore().runTransaction(async (transaction): Promise => { + const userRef = firebase + .firestore() + .collection(FirebasePath.FIREBASE_PATH_USERS) + .doc(userRecord.uid); + const userSnapshot = await transaction.get(userRef); + if (!userSnapshot.exists) { + console.log('Cannot retrieve user information'); + throw new HttpsError('unavailable', 'Cannot retrieve user information'); + } + const user = userSnapshot.data(); + transaction.delete(userRef); + console.log(`Deleted profile ${user.id}`); + }); +} diff --git a/functions/functions/tsconfig.json b/functions/functions/tsconfig.json new file mode 100644 index 00000000..14a4e6b9 --- /dev/null +++ b/functions/functions/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitAny": true, + "removeComments": true, + "strict": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "strictFunctionTypes": true, + "lib": [ + "es2017" + ], + "module": "commonjs", + "outDir": "lib", + "sourceMap": true, + "resolveJsonModule": true, + "target": "es2017" + }, + "compileOnSave": true, + "include": [ + "src" + ], + "files": [ + "node_modules/typescript/lib/lib.es6.d.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/functions/functions/tslint.json b/functions/functions/tslint.json new file mode 100644 index 00000000..9d157d09 --- /dev/null +++ b/functions/functions/tslint.json @@ -0,0 +1,11 @@ +{ + "extends": "tslint-config-airbnb", + "rules": { + "max-line-length": false, + "typedef": [ + true, + "call-signature", + "arrow-call-signature" + ] + } +} \ No newline at end of file diff --git a/functions/storage.rules b/functions/storage.rules new file mode 100644 index 00000000..9ec1d96e --- /dev/null +++ b/functions/storage.rules @@ -0,0 +1,9 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read: if request.auth != null; + allow write: if false; + } + } +} \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 00000000..e96ef602 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..f2872cf4 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..592ceee8 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..592ceee8 --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..31f9995e --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,495 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.chaomao.hitnotes; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.chaomao.hitnotes; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.chaomao.hitnotes; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..a28140cf --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..e882ab98 --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images": [ + { + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" + }, + { + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@3x.png", + "scale": "3x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@3x.png", + "scale": "3x" + }, + { + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" + }, + { + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@3x.png", + "scale": "3x" + }, + { + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@2x.png", + "scale": "2x" + }, + { + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@3x.png", + "scale": "3x" + }, + { + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@1x.png", + "scale": "1x" + }, + { + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" + }, + { + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" + }, + { + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" + }, + { + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@1x.png", + "scale": "1x" + }, + { + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" + }, + { + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@1x.png", + "scale": "1x" + }, + { + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@2x.png", + "scale": "2x" + }, + { + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "Icon-App-83.5x83.5@2x.png", + "scale": "2x" + }, + { + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "Icon-App-1024x1024@1x.png", + "scale": "1x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..28c6bf03 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f091b6b0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cde1211 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..d0ef06e7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..dcdc2306 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..c8f9ed8f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..75b2d164 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..c4df70d3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..6a84f41e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..d0e1f585 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..781d7cdc --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "LaunchImage.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "LaunchImage@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "LaunchImage@3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 00000000..b504c7ab --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + hitnotes + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/lib/authentication/authentication_bloc.dart b/lib/authentication/authentication_bloc.dart new file mode 100644 index 00000000..c1926ca2 --- /dev/null +++ b/lib/authentication/authentication_bloc.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_facebook_login/flutter_facebook_login.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +import '../user/user_repository.dart'; +import 'authentication_event.dart'; +import 'authentication_state.dart'; + +class AuthenticationBloc + extends Bloc { + AuthenticationBloc(this._userRepository) : super(Uninitialized()); + final _googleSignIn = GoogleSignIn(); + final _facebookLogin = FacebookLogin(); + final UserRepository _userRepository; + + @override + Stream mapEventToState( + AuthenticationEvent event, + ) async* { + if (event is SignInAnonymouslyEvent) { + yield* _mapSignInAnonymouslyEventToState(); + } else if (event is SignInWithGoogleEvent) { + yield* _mapSignInWithGoogleEventToState(); + } else if (event is SignInWithFacebookEvent) { + yield* _mapSignInWithFacebookEventToState(); + } + } + + Stream _mapSignInAnonymouslyEventToState() async* { + try { + await Firebase.initializeApp(); + final currentUser = FirebaseAuth.instance.currentUser; + if (currentUser == null) { + await FirebaseAuth.instance.signInAnonymously(); + } + _userRepository.changUser(); + yield Authenticated('Anonymous'); + } catch (_) { + yield Unauthenticated(); + } + } + + Stream _mapSignInWithGoogleEventToState() async* { + try { + final googleUser = await _googleSignIn.signIn(); + final googleAuth = await googleUser.authentication; + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + await _tryToLinkWithCurrentUser(credential); + _userRepository.changUser(); + yield Authenticated('Google'); + } on Exception { + yield Unauthenticated(); + } + } + + Stream _mapSignInWithFacebookEventToState() async* { + try { + final result = await _facebookLogin.logIn(['email', 'public_profile']); + switch (result.status) { + case FacebookLoginStatus.loggedIn: + final credential = + FacebookAuthProvider.credential(result.accessToken.token); + await _tryToLinkWithCurrentUser(credential); + _userRepository.changUser(); + yield Authenticated('Facebook'); + break; + default: + yield Unauthenticated(); + } + } on Exception { + yield Unauthenticated(); + } + } + + Future _tryToLinkWithCurrentUser(OAuthCredential authCredential) async { + try { + await FirebaseAuth.instance.currentUser + .linkWithCredential(authCredential); + } on Exception { + await FirebaseAuth.instance.signInWithCredential(authCredential); + } + } +} diff --git a/lib/authentication/authentication_event.dart b/lib/authentication/authentication_event.dart new file mode 100644 index 00000000..6b428b53 --- /dev/null +++ b/lib/authentication/authentication_event.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +abstract class AuthenticationEvent extends Equatable {} + +class SignInAnonymouslyEvent extends AuthenticationEvent { + @override + List get props => []; +} + +class SignInWithGoogleEvent extends AuthenticationEvent { + @override + List get props => []; +} + +class SignInWithFacebookEvent extends AuthenticationEvent { + @override + List get props => []; +} diff --git a/lib/authentication/authentication_state.dart b/lib/authentication/authentication_state.dart new file mode 100644 index 00000000..422d35d5 --- /dev/null +++ b/lib/authentication/authentication_state.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; + +abstract class AuthenticationState extends Equatable { + const AuthenticationState(); + + @override + List get props => []; +} + +class Uninitialized extends AuthenticationState {} + +class Authenticated extends AuthenticationState { + final String type; + + const Authenticated(this.type); + + @override + List get props => [type]; + + @override + String toString() => 'Authenticated { type: $type }'; +} + +class Unauthenticated extends AuthenticationState {} diff --git a/lib/extra_actions.dart b/lib/extra_actions.dart new file mode 100644 index 00000000..3c175601 --- /dev/null +++ b/lib/extra_actions.dart @@ -0,0 +1,123 @@ +import 'dart:io'; + +import 'package:device_info/device_info.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_email_sender/flutter_email_sender.dart'; +import 'package:in_app_review/in_app_review.dart'; +import 'package:package_info/package_info.dart'; +import 'package:share/share.dart'; + +import 'generated/l10n.dart'; +import 'routes.dart'; + +class ExtraActions extends StatelessWidget { + @override + Widget build(BuildContext context) { + return PopupMenuButton( + onSelected: (action) async { + switch (action) { + case ExtraAction.feedback: + final packageInfo = await PackageInfo.fromPlatform(); + final appName = packageInfo.appName; + final version = packageInfo.version; + final buildNumber = packageInfo.buildNumber; + var deviceInfo = ''; + if (Platform.isAndroid) { + final info = await DeviceInfoPlugin().androidInfo; + final release = info.version.release; + final sdkInt = info.version.sdkInt; + final manufacturer = info.manufacturer; + final model = info.model; + deviceInfo = + 'Android $release (SDK $sdkInt), $manufacturer $model'; + } + + if (Platform.isIOS) { + final info = await DeviceInfoPlugin().iosInfo; + final systemName = info.systemName; + final version = info.systemVersion; + final name = info.name; + final model = info.model; + deviceInfo = '$systemName $version, $name $model'; + } + final extendedBody = 'Name: $appName\n' + 'Version code: $buildNumber\n' + 'Version name: $version\n' + 'Device: $deviceInfo\n' + '---------------------\n'; + final email = Email( + body: extendedBody, + subject: '[$appName $version] Feedback', + recipients: ['chaomao.help@gmail.com'], + ); + await FlutterEmailSender.send(email); + break; + case ExtraAction.rateUs: + final _inAppReview = InAppReview.instance; + await _inAppReview.openStoreListing( + appStoreId: '_appStoreId', + microsoftStoreId: '_microsoftStoreId', + ); + break; + case ExtraAction.invite: + // FIXME Trick to create uriPrefix + final uriPrefix = + 'https://${Firebase.app().options.projectId.replaceAll('-', '')}.page.link'; + final parameters = DynamicLinkParameters( + uriPrefix: uriPrefix, + link: Uri.parse( + 'https://play.google.com/store/apps/details?id=com.chaomao.hittick'), + androidParameters: AndroidParameters( + packageName: 'com.chaomao.hittick', + minimumVersion: 0, + ), + dynamicLinkParametersOptions: DynamicLinkParametersOptions( + shortDynamicLinkPathLength: ShortDynamicLinkPathLength.short, + ), + iosParameters: IosParameters( + // FIXME + bundleId: 'com.google.FirebaseCppDynamicLinksTestApp.dev', + minimumVersion: '0', + ), + ); + final shortLink = await parameters.buildShortLink(); + final url = shortLink.shortUrl; + await Share.share(S.of(context).txt_invite_description(url), + subject: S.of(context).txt_dynamic_link_invite_subject); + break; + case ExtraAction.language: + await Navigator.pushNamed(context, Routes.language); + break; + case ExtraAction.theme: + await Navigator.pushNamed(context, Routes.theme); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: ExtraAction.feedback, + child: Text(S.of(context).txt_button_feedback), + ), + PopupMenuItem( + value: ExtraAction.rateUs, + child: Text(S.of(context).txt_about_rate), + ), + PopupMenuItem( + value: ExtraAction.invite, + child: Text(S.of(context).txt_about_invite), + ), + PopupMenuItem( + value: ExtraAction.language, + child: Text(S.of(context).txt_select_language), + ), + PopupMenuItem( + value: ExtraAction.theme, + child: Text(S.of(context).txt_theme), + ), + ], + ); + } +} + +enum ExtraAction { feedback, rateUs, invite, language, theme } diff --git a/lib/game/center_render_text_config.dart b/lib/game/center_render_text_config.dart new file mode 100644 index 00000000..d6d098ae --- /dev/null +++ b/lib/game/center_render_text_config.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +import 'package:flame/anchor.dart'; +import 'package:flame/position.dart'; +import 'package:flame/text_config.dart'; +import 'package:flutter/material.dart'; + +class CenterRenderTextConfig extends TextConfig { + CenterRenderTextConfig({ + double fontSize = 24.0, + Color color = Colors.black, + String fontFamily = 'Arial', + TextAlign textAlign = TextAlign.left, + TextDirection textDirection = TextDirection.ltr, + double lineHeight, + }) : super( + fontSize: fontSize, + color: color, + fontFamily: fontFamily, + textAlign: textAlign, + textDirection: textDirection, + lineHeight: lineHeight); + + @override + void render(Canvas canvas, String text, Position p, + {Anchor anchor = Anchor.topLeft}) { + final tp = toTextPainter(text); + final translatedPosition = anchor.translate(p, Position.fromSize(tp.size)); + translatedPosition.x -= tp.size.width / 2; + translatedPosition.y -= tp.size.height / 2; + tp.paint(canvas, translatedPosition.toOffset()); + } +} diff --git a/lib/game/complete_dialog.dart b/lib/game/complete_dialog.dart new file mode 100644 index 00000000..62364a26 --- /dev/null +++ b/lib/game/complete_dialog.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import '../generated/l10n.dart'; +import 'game_reward.dart'; + +class CompleteDialog extends StatelessWidget { + final GameReward _gameReward; + final Function() _onRestart; + + const CompleteDialog(this._gameReward, this._onRestart); + + @override + Widget build(BuildContext context) { + return Material( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Align( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image( + image: AssetImage((_gameReward.stars >= 1) + ? 'assets/images/img_star_rate.png' + : 'assets/images/img_star_rate_disable.png')), + SizedBox(width: 8), + Image( + image: AssetImage((_gameReward.stars >= 2) + ? 'assets/images/img_star_rate.png' + : 'assets/images/img_star_rate_disable.png')), + SizedBox(width: 8), + Image( + image: AssetImage((_gameReward.stars >= 3) + ? 'assets/images/img_star_rate.png' + : 'assets/images/img_star_rate_disable.png')), + ], + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image(image: AssetImage('assets/images/img_note.png')), + SizedBox(width: 8), + Text(_gameReward.playedNotes.toString()) + ], + ), + ], + )), + ), + Row( + children: [ + SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: Text(S.of(context).txt_button_quit), + ), + ), + SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () { + _onRestart(); + Navigator.of(context).pop(); + }, + child: Text(S.of(context).txt_button_restart)), + ), + SizedBox(width: 8) + ], + ) + ], + ), + ); + } +} diff --git a/lib/game/effect.dart b/lib/game/effect.dart new file mode 100644 index 00000000..8eea165e --- /dev/null +++ b/lib/game/effect.dart @@ -0,0 +1,11 @@ +import 'package:flutter/cupertino.dart'; + +abstract class Effect { + bool isDone() { + return false; + } + + void update(double delta) {} + + void render(Canvas canvas) {} +} diff --git a/lib/game/game_bloc.dart b/lib/game/game_bloc.dart new file mode 100644 index 00000000..3edc697f --- /dev/null +++ b/lib/game/game_bloc.dart @@ -0,0 +1,157 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:bloc/bloc.dart'; +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:collection/collection.dart'; +import 'package:dart_midi/dart_midi.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../midi_processor.dart'; +import '../util.dart'; +import 'game_event.dart'; +import 'game_reward.dart'; +import 'game_state.dart'; +import 'tile/tile_chunk.dart'; +import 'tile/tile_converter.dart'; + +class GameBloc extends Bloc { + GameBloc() : super(GameLoading()); + String _songName; + String _songId; + double _time = 0; + + int _maxTime; + int _tilesCount = 0; + double _speedDpsPerSecond; + List _tileChunks; + int _unitDuration; + var _errorCount = 0; + var _numberTileColumn = 0; + var _difficulty = 0; + var _speed = 0; + + final _pauseEventController = StreamController(); + + Stream get pauseStream => _pauseEventController.stream; + + final _completeEventController = StreamController(); + + Stream get completeStream => _completeEventController.stream; + + final _guideEventController = BehaviorSubject(); + + Stream get guideStream => _guideEventController.stream; + + @override + Stream mapEventToState(GameEvent event) async* { + if (event is StartGame) { + final directory = await getApplicationSupportDirectory(); + final song = event.song; + final tempFile = File('${directory.path}/${song.url}'); + if (!tempFile.existsSync()) { + await tempFile.create(recursive: true); + final task = FirebaseStorage.instance + .ref() + .child(song.url) + .writeToFile(tempFile); + await task; + } + final midiFile = MidiParser().parseMidiFromFile(tempFile); + _tileChunks = createTileChunks(midiFile); + final groupByDurationToPrevious = Map.fromEntries(groupBy(_tileChunks, + (TileChunk tileChunk) => tileChunk.durationToPrevious) + .entries + .toList() + ..sort((e1, e2) => e1.key.compareTo(e2.key))); + final countDurationToPrevious = { + for (var e in groupByDurationToPrevious.keys) + e: groupByDurationToPrevious[e].length + }; + + final sortCountDurationToPrevious = Map.fromEntries( + countDurationToPrevious.entries.toList() + ..sort((e1, e2) => e1.value.compareTo(e2.value))); + _unitDuration = sortCountDurationToPrevious.keys.last; + _difficulty = event.difficulty; + if (event.difficulty == 0) { + _numberTileColumn = 2; + } else if (event.difficulty == 1) { + _numberTileColumn = 3; + } else { + _numberTileColumn = 4; + } + _speed = event.speed; + var bpm = 0; + if (event.speed == 0) { + bpm = (song.bpm * 0.75).toInt(); + } else if (event.speed == 2) { + bpm = (song.bpm * 1.25).toInt(); + } else { + bpm = song.bpm; + } + final tiles = createTiles(_tileChunks, _unitDuration, _numberTileColumn); + final tick2Second = tickToSecond(midiFile.header.ticksPerBeat, bpm); + final speedDpsPerTick = UNIT_DURATION_HEIGHT / _unitDuration; + _speedDpsPerSecond = speedDpsPerTick / tick2Second; + final gameDuration = + (0.5 + (0.0 - tiles.last.initialY) / _speedDpsPerSecond).toInt(); + _songName = song.title; + _songId = song.id; + _maxTime = gameDuration; + final soundLoadedStream = MidiProcessor.getInstance().soundLoadedStream; + await for (final value in soundLoadedStream) { + if (value) { + yield GameStarted(tiles, _speedDpsPerSecond, _maxTime); + await Future.delayed(Duration(milliseconds: 500)); + yield GameUpdated(_tilesCount, _songName, _time, _maxTime); + return; + } + } + } else if (event is TileTouched) { + if (event.tile != null) { + await MidiProcessor.getInstance().playNote(event.tile.note); + _tilesCount += 1; + _time = (0.0 - event.tile.initialY) / _speedDpsPerSecond; + yield GameUpdated(_tilesCount, _songName, _time, _maxTime); + if (event.tile.y == pauseY) { + _errorCount++; + _guideEventController.add('txt_too_late'); + } else if (event.tile.y < pauseY - 120) { + _errorCount++; + _guideEventController.add('txt_too_early'); + } else { + _guideEventController.add(''); + } + } else { + _errorCount++; + _guideEventController.add('txt_too_many_fingers'); + } + } else if (event is PauseGame) { + _pauseEventController.add(true); + } else if (event is CompleteGame) { + await Future.delayed(Duration(milliseconds: 500)); + yield LoadingGift(); + final response = + await FirebaseFunctions.instance.httpsCallable('getGameReward').call({ + 'songId': _songId, + 'difficulty': _difficulty, + 'speed': _speed, + 'errorCount': _errorCount, + }); + final gameReward = + GameReward.fromJson(Map.from(response.data)); + _completeEventController.add(gameReward); + } else if (event is RestartGame) { + _time = 0.0; + _tilesCount = 0; + _errorCount = 0; + final tiles = createTiles(_tileChunks, _unitDuration, _numberTileColumn); + yield GameStarted(tiles, _speedDpsPerSecond, _maxTime); + await Future.delayed(Duration(milliseconds: 100)); + yield GameUpdated(_tilesCount, _songName, _time, _maxTime); + } + } +} diff --git a/lib/game/game_event.dart b/lib/game/game_event.dart new file mode 100644 index 00000000..b7b3d628 --- /dev/null +++ b/lib/game/game_event.dart @@ -0,0 +1,53 @@ +import 'package:equatable/equatable.dart'; + +import '../game/tile/tile.dart'; +import '../songs/song.dart'; + +abstract class GameEvent extends Equatable { + const GameEvent(); + + @override + List get props => []; +} + +class StartGame extends GameEvent { + final Song song; + final int difficulty; + final int speed; + + const StartGame(this.song, this.difficulty, this.speed); + + @override + List get props => [song, difficulty, speed]; + + @override + String toString() => 'StartGame { song: $song }'; +} + +class RestartGame extends GameEvent {} + +class PauseGame extends GameEvent { + const PauseGame(); + + @override + List get props => []; +} + +class CompleteGame extends GameEvent { + const CompleteGame(); + + @override + List get props => []; +} + +class TileTouched extends GameEvent { + final Tile tile; + + const TileTouched(this.tile); + + @override + List get props => [tile]; + + @override + String toString() => 'TileTouched { tile: $tile }'; +} diff --git a/lib/game/game_reward.dart b/lib/game/game_reward.dart new file mode 100644 index 00000000..f07d2f79 --- /dev/null +++ b/lib/game/game_reward.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'game_reward.g.dart'; + +@JsonSerializable(explicitToJson: true) +class GameReward { + int stars; + int playedNotes; + + GameReward( + this.stars, + this.playedNotes, + ); + + factory GameReward.fromJson(Map json) => + _$GameRewardFromJson(json); + + Map toJson() => _$GameRewardToJson(this); +} diff --git a/lib/game/game_reward.g.dart b/lib/game/game_reward.g.dart new file mode 100644 index 00000000..1377cdfc --- /dev/null +++ b/lib/game/game_reward.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'game_reward.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GameReward _$GameRewardFromJson(Map json) { + return GameReward( + json['stars'] as int, + json['playedNotes'] as int, + ); +} + +Map _$GameRewardToJson(GameReward instance) => + { + 'stars': instance.stars, + 'playedNotes': instance.playedNotes, + }; diff --git a/lib/game/game_state.dart b/lib/game/game_state.dart new file mode 100644 index 00000000..85383fb8 --- /dev/null +++ b/lib/game/game_state.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +import 'tile/tile.dart'; + +abstract class GameState extends Equatable { + const GameState(); + + @override + List get props => []; +} + +class GameLoading extends GameState {} + +class GameStarted extends GameState { + final List tiles; + final double speedPixelsPerSecond; + final int gameDuration; + + GameStarted(this.tiles, this.speedPixelsPerSecond, this.gameDuration); + + @override + List get props => [tiles, speedPixelsPerSecond, gameDuration]; +} + +class GameUpdated extends GameState { + final int tilesCount; + final String songName; + final double time; + final int maxTime; + + GameUpdated(this.tilesCount, this.songName, this.time, this.maxTime); + + @override + List get props => [tilesCount, songName, time, maxTime]; +} + +class LoadingGift extends GameState {} + +class GameNotLoaded extends GameState {} diff --git a/lib/game/game_widget.dart b/lib/game/game_widget.dart new file mode 100644 index 00000000..ecaac150 --- /dev/null +++ b/lib/game/game_widget.dart @@ -0,0 +1,253 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:percent_indicator/percent_indicator.dart'; + +import '../generated/l10n.dart'; +import '../main.dart'; +import 'complete_dialog.dart'; +import 'game_bloc.dart'; +import 'game_event.dart'; +import 'game_state.dart'; +import 'my_game.dart'; +import 'pause_dialog.dart'; +import 'tile/tile.dart'; +import 'tile/tile_converter.dart'; + +class GameWidget extends StatefulWidget { + final MyGame _game; + + GameWidget({Key key}) + : _game = MyGame(), + super(key: key); + + @override + _GameWidgetState createState() => _GameWidgetState(); +} + +class _GameWidgetState extends State { + @override + void initState() { + void _onRestart() { + BlocProvider.of(context).add(RestartGame()); + } + + BlocProvider.of(context).pauseStream.listen((event) { + showDialog( + context: context, + builder: (_) => PauseDialog(_onRestart), + ); + }); + BlocProvider.of(context).completeStream.listen((event) { + showDialog( + context: context, + builder: (_) => CompleteDialog(event, _onRestart), + ); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + void _onRestart() { + BlocProvider.of(context).add(RestartGame()); + } + + void _onTileTouched(Tile tile) { + BlocProvider.of(context).add(TileTouched(tile)); + } + + void _onCompleted() { + BlocProvider.of(context).add(CompleteGame()); + } + + return WillPopScope(onWillPop: () async { + await showDialog( + context: context, + builder: (_) => PauseDialog(_onRestart), + ); + return Future.value(false); + }, child: BlocBuilder( + builder: (context, state) { + if (state is GameLoading) { + final arguments = (ModalRoute.of(context).settings.arguments as Map); + final song = arguments['song']; + final difficulty = arguments['difficulty']; + final speed = arguments['speed']; + BlocProvider.of(context) + .add(StartGame(song, difficulty, speed)); + return LoadingSoundWidget(); + } else if (state is GameStarted) { + widget._game.start(state.tiles, state.speedPixelsPerSecond, + _onTileTouched, _onCompleted); + return LoadingSoundWidget(); + } else if (state is LoadingGift) { + return LoadingGiftWidget(); + } + return Stack(children: [ + widget._game.widget, + Container( + height: NON_TOUCH_REGION_HEIGHT.toDouble(), + child: Material( + color: Colors.transparent, + child: Column( + children: [ + LinearPercentIndicator( + padding: EdgeInsets.zero, + animation: true, + animationDuration: 0, + lineHeight: 8.0, + percent: (state as GameUpdated).time / + (state as GameUpdated).maxTime, + linearStrokeCap: LinearStrokeCap.butt, + progressColor: secondaryColor, + backgroundColor: onBackgroundColor.withOpacity(0.1), + ), + Padding( + padding: EdgeInsets.all(8), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text((state as GameUpdated).songName, + style: Theme.of(context).textTheme.headline6), + Text( + '${(state as GameUpdated).time.toInt() ~/ 60}:${((state as GameUpdated).time.toInt() % 60).toString().padLeft(2, '0')}/${(state as GameUpdated).maxTime.toInt() ~/ 60}:${((state as GameUpdated).maxTime.toInt() % 60).toString().padLeft(2, '0')}', + style: Theme.of(context).textTheme.headline6) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + iconSize: 38, + icon: Icon(Icons.pause_circle_outline_rounded), + onPressed: () { + BlocProvider.of(context) + .add(PauseGame()); + }, + ), + Text((state as GameUpdated).tilesCount.toString(), + style: Theme.of(context) + .textTheme + .headline4 + .copyWith(color: secondaryColor)) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [GuideTextWidget()], + ) + ], + ), + ) + ], + ), + )) + ]); + }, + )); + } +} + +class GuideTextWidget extends StatefulWidget { + @override + _GuideTextWidgetState createState() => _GuideTextWidgetState(); +} + +class _GuideTextWidgetState extends State { + String _text = ''; + StreamSubscription _userSubscription; + + @override + void initState() { + _userSubscription = + BlocProvider.of(context).guideStream.listen((event) { + setState(() { + if (event == 'txt_too_late') { + _text = S.of(context).txt_too_late; + } else if (event == 'txt_too_early') { + _text = S.of(context).txt_too_early; + } else if (event == 'txt_too_many_fingers') { + _text = S.of(context).txt_too_many_fingers; + } else if (event == '') { + _text = ''; + } + }); + }); + super.initState(); + } + + @override + void dispose() { + _userSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text('$_text', + style: Theme.of(context) + .textTheme + .headline5 + .copyWith(color: primaryColor)); + } +} + +class LoadingSoundWidget extends StatelessWidget { + const LoadingSoundWidget({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.center, + child: Image( + image: AssetImage('assets/images/img_app_icon.png')), + ), + ), + Text(S.of(context).txt_dialog_loading_sound_description) + ], + )), + ); + } +} + +class LoadingGiftWidget extends StatelessWidget { + const LoadingGiftWidget({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.center, + child: Image( + image: AssetImage('assets/images/img_app_icon.png')), + ), + ), + Text(S.of(context).txt_game_complete_loading_gift) + ], + )), + ); + } +} diff --git a/lib/game/my_game.dart b/lib/game/my_game.dart new file mode 100644 index 00000000..3dac06d0 --- /dev/null +++ b/lib/game/my_game.dart @@ -0,0 +1,132 @@ +import 'package:flame/game.dart'; +import 'package:flame/gestures.dart'; +import 'package:flame/position.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../main.dart'; +import '../util.dart'; +import 'effect.dart'; +import 'tile/tile.dart'; +import 'tile/tile_effect_spawner.dart'; +import 'tile/tile_input_handler.dart'; +import 'tile/tiles_controller.dart'; + +class MyGame extends Game with MultiTouchTapDetector { + final _tilesController = TilesController(); + final _tileEffects = []; + var _state = _MyGameState.PREPARE; + var _accumulator = 0.0; + final _step = 1.0 / 60.0; + final Map _touches = {}; + Function(Tile tile) _onTouched; + Function() _onCompleted; + final _backgroundPaint = Paint()..color = backgroundColor; + final _grayPaint = Paint() + ..colorFilter = ColorFilter.mode(onBackgroundColor, BlendMode.srcIn); + + final _staffSprite = Sprite('${nearestDevicePixelRatioFolder}img_staff.png'); + final _clefSprite = Sprite('${nearestDevicePixelRatioFolder}img_clef.png'); + + void start(List tiles, double speedPixelsPerSecond, + Function(Tile tile) onTouched, Function() onCompleted) async { + _tilesController.initialize(tiles, speedPixelsPerSecond); + _state = _MyGameState.PLAY; + _onTouched = onTouched; + _onCompleted = onCompleted; + _accumulator = 0.0; + } + + void pause() {} + + @override + void onTapDown(int pointerId, TapDownDetails details) { + _touches[pointerId] = + _TouchPosition(details.globalPosition.dx, details.globalPosition.dy); + } + + @override + void onTapUp(int pointerId, _) { + _touches.remove(pointerId); + } + + @override + void render(Canvas canvas) { + final rect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); + canvas.drawRect(rect, _backgroundPaint); + _staffSprite.renderPosition(canvas, Position(0.0, pauseY - 96 + 24), + size: Position(screenWidth, 96), overridePaint: _grayPaint); + _clefSprite.renderPosition(canvas, Position(0.0, pauseY - 96 + 24), + size: Position(96, 96), overridePaint: _grayPaint); + _tilesController.render(canvas); + _tileEffects.forEach((effect) { + effect.render(canvas); + }); + } + + @override + void update(double delta) { + // // FIXME Auto run + // for (var i =0; i < _tilesController.tiles.length;i++) { + // final tile = _tilesController.tiles[i]; + // if (tile != null && tile.y >= pauseY - 120) { + // _touches[i] = _TouchPosition(0, 0); + // } else { + // break; + // } + // } + _tileEffects.forEach((effect) { + effect.update(delta); + }); + _tileEffects.removeWhere((effect) => effect.isDone()); + if (_state != _MyGameState.STOP) { + /* Max frame time to avoid spiral of death */ + final restrictedTime = (delta > 0.25) ? 0.25 : delta; + _accumulator += restrictedTime; + while (_accumulator >= _step) { + var initialYAllowedTouch = double.negativeInfinity; + _touches.forEach((key, value) { + final tile = _tilesController.getNextUntouchedTile(); + if (tile != null) { + if (tile.initialY >= initialYAllowedTouch) { + tile.touchDown(); + _tileEffects.addAll(tile.getEffects()); + _onTouched(tile); + if (_state == _MyGameState.PAUSE) { + _state = _MyGameState.PLAY; + } + initialYAllowedTouch = tile.initialY; + } else { + _onTouched(null); + } + } else { + _onTouched(null); + } + }); + _touches.clear(); + _accumulator -= _step; + if (_state == _MyGameState.PLAY) { + final actualDeltaTime = _tilesController.tryUpdate(_step); + if (_step > actualDeltaTime) { + pause(); + } + if (_tilesController.tiles.isEmpty) { + _state = _MyGameState.COMPLETE; + _onCompleted(); + } + } + } + } + } +} + +enum _MyGameState { PREPARE, PLAY, PAUSE, STOP, COMPLETE } + +class _TouchPosition { + final double x; + final double y; + + _TouchPosition(this.x, this.y); +} diff --git a/lib/game/note/note.dart b/lib/game/note/note.dart new file mode 100644 index 00000000..697985c6 --- /dev/null +++ b/lib/game/note/note.dart @@ -0,0 +1,62 @@ +import 'package:meta/meta.dart'; + +class Note { + final int note; + final int startTick; + +// + + const Note({ + @required this.note, + @required this.startTick, + }); + + Note copyWith({ + int note, + int startTick, + }) { + if ((note == null || identical(note, this.note)) && + (startTick == null || identical(startTick, this.startTick))) { + return this; + } + + return new Note( + note: note ?? this.note, + startTick: startTick ?? this.startTick, + ); + } + + @override + String toString() { + return 'Note{note: $note, startTick: $startTick}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Note && + runtimeType == other.runtimeType && + note == other.note && + startTick == other.startTick); + + @override + int get hashCode => note.hashCode ^ startTick.hashCode; + + factory Note.fromMap(Map map) { + return new Note( + note: map['note'] as int, + startTick: map['startTick'] as int, + ); + } + + Map toMap() { + // ignore: unnecessary_cast + return { + 'note': this.note, + 'startTick': this.startTick, + } as Map; + } + +// + +} diff --git a/lib/game/pause_dialog.dart b/lib/game/pause_dialog.dart new file mode 100644 index 00000000..5093ba72 --- /dev/null +++ b/lib/game/pause_dialog.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import '../generated/l10n.dart'; + +class PauseDialog extends StatelessWidget { + final Function() _onRestart; + + const PauseDialog(this._onRestart); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: Text(S.of(context).txt_button_quit), + ), + ElevatedButton( + onPressed: () { + _onRestart(); + Navigator.of(context).pop(); + }, + child: Text(S.of(context).txt_button_restart), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(S.of(context).txt_game_button_continue), + ) + ], + ), + ); + } +} diff --git a/lib/game/ripple_effect.dart b/lib/game/ripple_effect.dart new file mode 100644 index 00000000..f989d6eb --- /dev/null +++ b/lib/game/ripple_effect.dart @@ -0,0 +1,55 @@ +import 'dart:ui'; + +import 'package:flame/position.dart'; +import 'package:flame/sprite.dart'; + +import '../main.dart'; +import '../util.dart'; +import 'effect.dart'; + +Sprite touchSprite = Sprite('${nearestDevicePixelRatioFolder}img_touch.png'); +Paint paint = Paint() + ..colorFilter = + ColorFilter.mode(primaryColor.withOpacity(0.1), BlendMode.srcIn); + +class RippleEffect extends Effect { + static const LIVE_TIME = 0.5; + static const SIZE = 96; + final initialWidth = 0.5 * SIZE; + var _width; + var _rate; + var _time = 0.0; + var _isDone = false; + final _x; + final _y; + + RippleEffect(this._x, this._y) { + _rate = (SIZE - initialWidth) / LIVE_TIME; + _width = initialWidth; + } + + @override + bool isDone() { + return _isDone; + } + + @override + void update(double delta) { + _time += delta; + _width = initialWidth + _rate * _time; + + if (_time > LIVE_TIME) { + _isDone = true; + } + } + + @override + void render(Canvas canvas) { + touchSprite.renderPosition( + canvas, + Position((_x - _width / 2).toInt().toDouble(), + (_y - _width / 2).toInt().toDouble()), + size: Position(_width.toInt().toDouble(), _width.toInt().toDouble()), + overridePaint: paint); + } +} diff --git a/lib/game/tile/tile.dart b/lib/game/tile/tile.dart new file mode 100644 index 00000000..9e217b0f --- /dev/null +++ b/lib/game/tile/tile.dart @@ -0,0 +1,16 @@ +import 'tile_converter.dart'; + +class Tile { + final int note; + final double initialY; + double y = 0.0; + TileState state = TileState.UNTOUCHED; + final double width = TILE_WIDTH; + final double height = TILE_HEIGHT; + Function(Tile tile) onTouched; + final int column; + + Tile(this.note, this.column, this.initialY); +} + +enum TileState { UNTOUCHED, TOUCHED } diff --git a/lib/game/tile/tile_chunk.dart b/lib/game/tile/tile_chunk.dart new file mode 100644 index 00000000..444a2caf --- /dev/null +++ b/lib/game/tile/tile_chunk.dart @@ -0,0 +1,74 @@ +import 'package:meta/meta.dart'; + +import '../note/note.dart'; + +class TileChunk { + final List notes; + final int durationToPrevious; + final int startTick; + + // + + const TileChunk({ + @required this.notes, + @required this.durationToPrevious, + @required this.startTick, + }); + + TileChunk copyWith({ + List notes, + int durationToPrevious, + int startTick, + }) { + if ((notes == null || identical(notes, this.notes)) && + (durationToPrevious == null || + identical(durationToPrevious, this.durationToPrevious)) && + (startTick == null || identical(startTick, this.startTick))) { + return this; + } + + return new TileChunk( + notes: notes ?? this.notes, + durationToPrevious: durationToPrevious ?? this.durationToPrevious, + startTick: startTick ?? this.startTick, + ); + } + + @override + String toString() { + return 'TileChunk{notes: $notes, durationToPrevious: $durationToPrevious, startTick: $startTick}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TileChunk && + runtimeType == other.runtimeType && + notes == other.notes && + durationToPrevious == other.durationToPrevious && + startTick == other.startTick); + + @override + int get hashCode => + notes.hashCode ^ durationToPrevious.hashCode ^ startTick.hashCode; + + factory TileChunk.fromMap(Map map) { + return new TileChunk( + notes: map['notes'] as List, + durationToPrevious: map['durationToPrevious'] as int, + startTick: map['startTick'] as int, + ); + } + + Map toMap() { + // ignore: unnecessary_cast + return { + 'notes': this.notes, + 'durationToPrevious': this.durationToPrevious, + 'startTick': this.startTick, + } as Map; + } + +// + +} diff --git a/lib/game/tile/tile_converter.dart b/lib/game/tile/tile_converter.dart new file mode 100644 index 00000000..b1895013 --- /dev/null +++ b/lib/game/tile/tile_converter.dart @@ -0,0 +1,88 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:dart_midi/dart_midi.dart'; + +import '../note/note.dart'; +import '../tile/tile.dart'; +import '../tile/tile_chunk.dart'; + +const MINUTE_TO_SECOND = 60; +const NUMBER_OF_NOTES = 128; +const NUMBER_TILE_COLUMN = 4; +const UNIT_DURATION_HEIGHT = 72; +const TILE_WIDTH = 36.0; +const TILE_HEIGHT = TILE_WIDTH; +const NON_TOUCH_REGION_HEIGHT = 150; +const startVisibleY = 0; + +double tickToSecond(int resolution, int bpm) { + return MINUTE_TO_SECOND.toDouble() / (resolution * bpm); +} + +List createTileChunks(MidiFile midiFile) { + final tileNotes = []; + final onsets = List.filled(NUMBER_OF_NOTES, -1, growable: false); + for (final midiTrack in midiFile.tracks) { + var totalTicks = 0; + for (final midiEvent in midiTrack) { + totalTicks += midiEvent.deltaTime; + if (midiEvent is NoteOnEvent) { + final noteValue = midiEvent.noteNumber; + if (onsets[noteValue] == -1) { + onsets[noteValue] = totalTicks; + } + } else if (midiEvent is NoteOffEvent) { + final noteValue = midiEvent.noteNumber; + if (onsets[noteValue] >= 0) { + tileNotes.add(Note(note: noteValue, startTick: onsets[noteValue])); + onsets[noteValue] = -1; + } + } + } + } + final tileChunks = []; + + var previousStartTick = 0; + Map.fromEntries( + groupBy(tileNotes, (Note note) => note.startTick).entries.toList() + ..sort((e1, e2) => e1.key.compareTo(e2.key))) + .forEach((key, value) { + tileChunks.add(TileChunk( + notes: value, + durationToPrevious: value[0].startTick - previousStartTick, + startTick: value[0].startTick)); + previousStartTick = value[0].startTick; + }); + return tileChunks; +} + +List createTiles( + List tileChunks, + int unitDuration, + int numberTileColumn, +) { + final tiles = []; + final random = Random(); + var calibratedTick = 0; + for (final chunk in tileChunks) { + var tileColumn = (numberTileColumn <= chunk.notes.length) + ? 0 + : random.nextInt(numberTileColumn - chunk.notes.length); + if (chunk.durationToPrevious < unitDuration) { + /* Calibrate to make sure a note will away from previous note at least unitTileDuration */ + calibratedTick += unitDuration - chunk.durationToPrevious; + } + final initialPositionY = + -((UNIT_DURATION_HEIGHT / unitDuration) * chunk.startTick + + calibratedTick); + chunk.notes.asMap().forEach((index, note) { + if (index < numberTileColumn) { + final tile = Tile(note.note, tileColumn, initialPositionY); + tiles.add(tile); + tileColumn++; + } + }); + } + return tiles; +} diff --git a/lib/game/tile/tile_drawer.dart b/lib/game/tile/tile_drawer.dart new file mode 100644 index 00000000..abb0ff76 --- /dev/null +++ b/lib/game/tile/tile_drawer.dart @@ -0,0 +1,19 @@ +import 'package:flame/position.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/cupertino.dart'; + +import '../../main.dart'; +import '../../util.dart'; +import 'tile.dart'; + +Sprite noteSprite = + Sprite('${nearestDevicePixelRatioFolder}img_single_note.png'); + +extension TileDrawer on Tile { + void render(Canvas canvas) { + noteSprite.renderPosition(canvas, + Position(positionsX[column].toInt().toDouble(), y.toInt().toDouble()), + size: Position(width.toInt().toDouble(), height.toInt().toDouble()), + overridePaint: paint); + } +} diff --git a/lib/game/tile/tile_effect_spawner.dart b/lib/game/tile/tile_effect_spawner.dart new file mode 100644 index 00000000..835b4853 --- /dev/null +++ b/lib/game/tile/tile_effect_spawner.dart @@ -0,0 +1,14 @@ +import '../../util.dart'; +import '../effect.dart'; +import '../ripple_effect.dart'; +import 'tile.dart'; +import 'tile_touch_effect.dart'; + +extension TileEffectSpawner on Tile { + List getEffects() { + return [ + RippleEffect(positionsX[column] + width / 2, y + height / 2), + TileTouchEffect(this) + ]; + } +} diff --git a/lib/game/tile/tile_input_handler.dart b/lib/game/tile/tile_input_handler.dart new file mode 100644 index 00000000..178cbbf9 --- /dev/null +++ b/lib/game/tile/tile_input_handler.dart @@ -0,0 +1,7 @@ +import 'tile.dart'; + +extension TileInputHandler on Tile { + void touchDown() { + state = TileState.TOUCHED; + } +} diff --git a/lib/game/tile/tile_touch_effect.dart b/lib/game/tile/tile_touch_effect.dart new file mode 100644 index 00000000..7890007b --- /dev/null +++ b/lib/game/tile/tile_touch_effect.dart @@ -0,0 +1,133 @@ +import 'package:flame/position.dart'; +import 'package:flutter/cupertino.dart'; + +import '../../main.dart'; +import '../../util.dart'; +import '../center_render_text_config.dart'; +import '../effect.dart'; + +const LIVE_TIME = 0.5; + +const noteToName = { + 21: 'A0', + 22: 'A#0', + 23: 'B0', + 24: 'C1', + 25: 'C#1', + 26: 'D1', + 27: 'D#1', + 28: 'E1', + 29: 'F1', + 30: 'F#1', + 31: 'G1', + 32: 'G#1', + 33: 'A1', + 34: 'A#1', + 35: 'B1', + 36: 'C2', + 37: 'C#2', + 38: 'D2', + 39: 'D#2', + 40: 'E2', + 41: 'F2', + 42: 'F#2', + 43: 'G2', + 44: 'G#2', + 45: 'A2', + 46: 'A#2', + 47: 'B2', + 48: 'C3', + 49: 'C#3', + 50: 'D3', + 51: 'D#3', + 52: 'E3', + 53: 'F3', + 54: 'F#3', + 55: 'G3', + 56: 'G#3', + 57: 'A3', + 58: 'A#3', + 59: 'B3', + 60: 'C4', + 61: 'C#4', + 62: 'D4', + 63: 'D#4', + 64: 'E4', + 65: 'F4', + 66: 'F#4', + 67: 'G4', + 68: 'G#4', + 69: 'A4', + 70: 'A#4', + 71: 'B4', + 72: 'C5', + 73: 'C#5', + 74: 'D5', + 75: 'D#5', + 76: 'E5', + 77: 'F5', + 78: 'F#5', + 79: 'G5', + 80: 'G#5', + 81: 'A5', + 82: 'A#5', + 83: 'B5', + 84: 'C6', + 85: 'C#6', + 86: 'D6', + 87: 'D#6', + 88: 'E6', + 89: 'F6', + 90: 'F#6', + 91: 'G6', + 92: 'G#6', + 93: 'A6', + 94: 'A#6', + 95: 'B6', + 96: 'C7', + 97: 'C#7', + 98: 'D7', + 99: 'D#7', + 100: 'E7', + 101: 'F7', + 102: 'F#7', + 103: 'G7', + 104: 'G#7', + 105: 'A7', + 106: 'A#7', + 107: 'B7', + 108: 'C8' +}; + +class TileTouchEffect extends Effect { + final config = CenterRenderTextConfig(fontSize: 24.0, color: primaryColor); + var _time = 0.0; + var _isDone = false; + final _centerX; + final _centerY; + final _text; + + TileTouchEffect(_tile) + : _centerX = positionsX[_tile.column] + _tile.width / 2.0, + _centerY = _tile.y + _tile.height / 2.0, + _text = noteToName[_tile.note]; + + @override + bool isDone() { + return _isDone; + } + + @override + void update(double delta) { + _time += delta; + if (_time > LIVE_TIME) { + _isDone = true; + } + } + + @override + void render(Canvas canvas) { + config.render(canvas, _text, + Position(_centerX.toInt().toDouble(), _centerY.toInt().toDouble())); + } +} diff --git a/lib/game/tile/tile_updater.dart b/lib/game/tile/tile_updater.dart new file mode 100644 index 00000000..9be121ef --- /dev/null +++ b/lib/game/tile/tile_updater.dart @@ -0,0 +1,7 @@ +import 'tile.dart'; + +extension TileUpdater on Tile { + void updateY(double deltaY) { + y = initialY + deltaY; + } +} diff --git a/lib/game/tile/tiles_controller.dart b/lib/game/tile/tiles_controller.dart new file mode 100644 index 00000000..c9bab8da --- /dev/null +++ b/lib/game/tile/tiles_controller.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import '../../util.dart'; +import 'tile.dart'; +import 'tile_converter.dart'; +import 'tile_drawer.dart'; +import 'tile_updater.dart'; + +class TilesController { + var _visibleTileCount = 0; + double _deltaY = 0; + double _speedPixelsPerSecond = 0; + List tiles = []; + + void initialize(List tiles, double speedPixelsPerSecond) { + _speedPixelsPerSecond = speedPixelsPerSecond; + _visibleTileCount = 0; + _deltaY = 0; + this.tiles = tiles; + } + + double tryUpdate(double delta) { + _tryToMakeTilesVisible(); + final maxDeltaTime = _getMaxDeltaY() / _speedPixelsPerSecond; + final actualDelta = (delta >= maxDeltaTime) ? maxDeltaTime : delta; + + _deltaY += _speedPixelsPerSecond * actualDelta; + for (var i = 0; i < _visibleTileCount; i++) { + tiles[i].updateY(_deltaY); + } + return actualDelta; + } + + void _tryToMakeTilesVisible() { + var end = 0; + final iterator = tiles.iterator; + while (iterator.moveNext()) { + final tile = iterator.current; + if (tile.state == TileState.TOUCHED) { + _visibleTileCount--; + end++; + } else { + break; + } + } + tiles.removeRange(0, end); + for (var i = _visibleTileCount; i < tiles.length; i++) { + final tile = tiles[i]; + if (tile.initialY + _deltaY >= startVisibleY) { + tile.y = tile.initialY + _deltaY; + _visibleTileCount += 1; + } else { + break; + } + } + } + + double _getMaxDeltaY() { + return pauseY - + tiles + .firstWhere((element) => element.state == TileState.UNTOUCHED, + orElse: () => Tile(0, 0, 0)) + .y; + } + + Tile getNextUntouchedTile() { + for (var i = 0; i < _visibleTileCount; i++) { + final tile = tiles[i]; + if (tile.state == TileState.UNTOUCHED) { + return tile; + } + } + return null; + } + + void render(Canvas canvas) { + for (var i = 0; i < _visibleTileCount; i++) { + canvas.save(); + tiles[i].render(canvas); + canvas.restore(); + } + } +} diff --git a/lib/game_config/game_config_bloc.dart b/lib/game_config/game_config_bloc.dart new file mode 100644 index 00000000..b4b4bbd1 --- /dev/null +++ b/lib/game_config/game_config_bloc.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'game_config_event.dart'; +part 'game_config_state.dart'; + +class GameConfigBloc extends Bloc { + GameConfigBloc() : super(GameConfigState(1, 1)); + + @override + Stream mapEventToState( + GameConfigEvent event, + ) async* { + if (event is GameConfigChangeDifficultyEvent) { + yield GameConfigState(event.difficulty, state.speed); + } else if (event is GameConfigChangeSpeedEvent) { + yield GameConfigState(state.difficulty, event.speed); + } + } +} diff --git a/lib/game_config/game_config_event.dart b/lib/game_config/game_config_event.dart new file mode 100644 index 00000000..dca3e06e --- /dev/null +++ b/lib/game_config/game_config_event.dart @@ -0,0 +1,23 @@ +part of 'game_config_bloc.dart'; + +abstract class GameConfigEvent extends Equatable { + const GameConfigEvent(); +} + +class GameConfigChangeDifficultyEvent extends GameConfigEvent { + final int difficulty; + + const GameConfigChangeDifficultyEvent(this.difficulty); + + @override + List get props => [difficulty]; +} + +class GameConfigChangeSpeedEvent extends GameConfigEvent { + final int speed; + + const GameConfigChangeSpeedEvent(this.speed); + + @override + List get props => [speed]; +} diff --git a/lib/game_config/game_config_state.dart b/lib/game_config/game_config_state.dart new file mode 100644 index 00000000..5eedd99d --- /dev/null +++ b/lib/game_config/game_config_state.dart @@ -0,0 +1,11 @@ +part of 'game_config_bloc.dart'; + +class GameConfigState extends Equatable { + final int difficulty; + final int speed; + + const GameConfigState(this.difficulty, this.speed); + + @override + List get props => [difficulty, speed]; +} diff --git a/lib/game_config/game_config_widget.dart b/lib/game_config/game_config_widget.dart new file mode 100644 index 00000000..81347ac1 --- /dev/null +++ b/lib/game_config/game_config_widget.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../generated/l10n.dart'; +import '../routes.dart'; +import '../songs/song.dart'; +import 'game_config_bloc.dart'; + +class GameConfigWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final song = ModalRoute.of(context).settings.arguments as Song; + return BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).txt_configure, + style: Theme.of(context).appBarTheme.textTheme.headline5)), + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(S.of(context).txt_difficulty, + style: Theme.of(context).textTheme.headline5), + SizedBox(height: 8), + Row( + children: [ + SizedBox(width: 8), + CardWidget( + selected: state.difficulty == 0, + text: S.of(context).txt_easy, + caption: S.of(context).txt_fingers(2), + onTap: () { + BlocProvider.of(context) + ..add(GameConfigChangeDifficultyEvent(0)); + }, + ), + SizedBox(width: 8), + CardWidget( + selected: state.difficulty == 1, + text: S.of(context).txt_medium, + caption: S.of(context).txt_fingers(3), + onTap: () { + BlocProvider.of(context) + ..add(GameConfigChangeDifficultyEvent(1)); + }, + ), + SizedBox(width: 8), + CardWidget( + selected: state.difficulty == 2, + text: S.of(context).txt_difficult, + caption: S.of(context).txt_fingers(4), + onTap: () { + BlocProvider.of(context) + ..add(GameConfigChangeDifficultyEvent(2)); + }, + ), + SizedBox(width: 8) + ], + ) + ], + ), + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(S.of(context).txt_speed, + style: Theme.of(context).textTheme.headline5), + SizedBox(height: 8), + Row( + children: [ + SizedBox(width: 8), + CardWidget( + selected: state.speed == 0, + text: S.of(context).txt_slow, + caption: 'x0.75', + onTap: () { + BlocProvider.of(context) + ..add(GameConfigChangeSpeedEvent(0)); + }, + ), + SizedBox(width: 8), + CardWidget( + selected: state.speed == 1, + text: S.of(context).txt_normal, + caption: 'x1.0', + onTap: () { + BlocProvider.of(context) + ..add(GameConfigChangeSpeedEvent(1)); + }, + ), + SizedBox(width: 8), + CardWidget( + selected: state.speed == 2, + text: S.of(context).txt_fast, + caption: 'x1.25', + onTap: () { + BlocProvider.of(context) + ..add(GameConfigChangeSpeedEvent(2)); + }, + ), + SizedBox(width: 8) + ], + ) + ], + ), + ), + Row( + children: [ + SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, Routes.game, arguments: { + 'song': song, + 'difficulty': state.difficulty, + 'speed': state.speed + }); + }, + child: Text(S.of(context).txt_start)), + ), + SizedBox(width: 8) + ], + ) + ], + )); + }); + } +} + +class CardWidget extends StatelessWidget { + final bool selected; + final String text; + final String caption; + final GestureTapCallback onTap; + + const CardWidget({ + Key key, + @required this.selected, + @required this.text, + @required this.caption, + @required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: selected + ? ElevatedButton( + onPressed: () { + onTap(); + }, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 8), + Text(text), + SizedBox(height: 8), + Text(caption), + SizedBox(height: 8), + ], + )) + : OutlinedButton( + onPressed: () { + onTap(); + }, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 8), + Text(text), + SizedBox(height: 8), + Text(caption), + SizedBox(height: 8), + ], + ))); + } +} diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart new file mode 100644 index 00000000..b62fe0f4 --- /dev/null +++ b/lib/generated/intl/messages_all.dart @@ -0,0 +1,75 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that looks up messages for specific locales by +// delegating to the appropriate library. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:implementation_imports, file_names, unnecessary_new +// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering +// ignore_for_file:argument_type_not_assignable, invalid_assignment +// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases +// ignore_for_file:comment_references + +import 'dart:async'; + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; +import 'package:intl/src/intl_helpers.dart'; + +import 'messages_en.dart' as messages_en; +import 'messages_ko.dart' as messages_ko; +import 'messages_vi.dart' as messages_vi; +import 'messages_zh.dart' as messages_zh; + +typedef Future LibraryLoader(); +Map _deferredLibraries = { + 'en': () => new Future.value(null), + 'ko': () => new Future.value(null), + 'vi': () => new Future.value(null), + 'zh': () => new Future.value(null), +}; + +MessageLookupByLibrary _findExact(String localeName) { + switch (localeName) { + case 'en': + return messages_en.messages; + case 'ko': + return messages_ko.messages; + case 'vi': + return messages_vi.messages; + case 'zh': + return messages_zh.messages; + default: + return null; + } +} + +/// User programs should call this before using [localeName] for messages. +Future initializeMessages(String localeName) async { + var availableLocale = Intl.verifiedLocale( + localeName, + (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); + if (availableLocale == null) { + return new Future.value(false); + } + var lib = _deferredLibraries[availableLocale]; + await (lib == null ? new Future.value(false) : lib()); + initializeInternalMessageLookup(() => new CompositeMessageLookup()); + messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); + return new Future.value(true); +} + +bool _messagesExistFor(String locale) { + try { + return _findExact(locale) != null; + } catch (e) { + return false; + } +} + +MessageLookupByLibrary _findGeneratedMessagesFor(String locale) { + var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, + onFailure: (_) => null); + if (actualLocale == null) return null; + return _findExact(actualLocale); +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart new file mode 100644 index 00000000..4f047a8b --- /dev/null +++ b/lib/generated/intl/messages_en.dart @@ -0,0 +1,84 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a en locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'en'; + + static m0(input) => "Reached level ${input}"; + + static m1(input) => "${input} fingers"; + + static m2(input) => "Hey, let play Hit Notes ${input}"; + + static m3(input) => "Joined ${input}"; + + static m4(input) => "Level ${input}"; + + static m5(input) => "Using ${input}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static _notInlinedMessages(_) => { + "acoustic_guitar" : MessageLookupByLibrary.simpleMessage("Acoustic Guitar"), + "classic" : MessageLookupByLibrary.simpleMessage("Classic"), + "dark" : MessageLookupByLibrary.simpleMessage("Dark"), + "electric_guitar" : MessageLookupByLibrary.simpleMessage("Electric Guitar"), + "folk" : MessageLookupByLibrary.simpleMessage("Folk"), + "kpop" : MessageLookupByLibrary.simpleMessage("Kpop"), + "light" : MessageLookupByLibrary.simpleMessage("Light"), + "other" : MessageLookupByLibrary.simpleMessage("Other genre"), + "piano" : MessageLookupByLibrary.simpleMessage("Piano"), + "pop" : MessageLookupByLibrary.simpleMessage("Pop"), + "system" : MessageLookupByLibrary.simpleMessage("System default"), + "txt_about_invite" : MessageLookupByLibrary.simpleMessage("Invite"), + "txt_about_rate" : MessageLookupByLibrary.simpleMessage("Rate us"), + "txt_all_ok" : MessageLookupByLibrary.simpleMessage("Ok"), + "txt_all_songs" : MessageLookupByLibrary.simpleMessage("All songs"), + "txt_button_feedback" : MessageLookupByLibrary.simpleMessage("Feedback us"), + "txt_button_quit" : MessageLookupByLibrary.simpleMessage("Quit"), + "txt_button_restart" : MessageLookupByLibrary.simpleMessage("Play again"), + "txt_button_sign_in_facebook" : MessageLookupByLibrary.simpleMessage("Sign in with Facebook"), + "txt_button_sign_in_google" : MessageLookupByLibrary.simpleMessage("Sign in with Google"), + "txt_configure" : MessageLookupByLibrary.simpleMessage("Configure"), + "txt_dialog_level_up_description" : m0, + "txt_dialog_loading_sound_description" : MessageLookupByLibrary.simpleMessage("Loading sound…"), + "txt_difficult" : MessageLookupByLibrary.simpleMessage("Difficult"), + "txt_difficulty" : MessageLookupByLibrary.simpleMessage("Difficulty"), + "txt_dynamic_link_invite_subject" : MessageLookupByLibrary.simpleMessage("Hit Notes invite"), + "txt_easy" : MessageLookupByLibrary.simpleMessage("Easy"), + "txt_fast" : MessageLookupByLibrary.simpleMessage("Fast"), + "txt_fingers" : m1, + "txt_game_button_continue" : MessageLookupByLibrary.simpleMessage("Continue"), + "txt_game_complete_loading_gift" : MessageLookupByLibrary.simpleMessage("Getting reward…"), + "txt_instrument_title_instruments" : MessageLookupByLibrary.simpleMessage("Instruments"), + "txt_invite_description" : m2, + "txt_joined" : m3, + "txt_level" : m4, + "txt_medium" : MessageLookupByLibrary.simpleMessage("Medium"), + "txt_normal" : MessageLookupByLibrary.simpleMessage("Normal"), + "txt_page_title_account" : MessageLookupByLibrary.simpleMessage("Account"), + "txt_select_language" : MessageLookupByLibrary.simpleMessage("Select language"), + "txt_slow" : MessageLookupByLibrary.simpleMessage("Slow"), + "txt_speed" : MessageLookupByLibrary.simpleMessage("Speed"), + "txt_start" : MessageLookupByLibrary.simpleMessage("Start"), + "txt_theme" : MessageLookupByLibrary.simpleMessage("Theme"), + "txt_too_early" : MessageLookupByLibrary.simpleMessage("Too early"), + "txt_too_late" : MessageLookupByLibrary.simpleMessage("Too late"), + "txt_too_many_fingers" : MessageLookupByLibrary.simpleMessage("Too many fingers"), + "txt_using" : m5 + }; +} diff --git a/lib/generated/intl/messages_ko.dart b/lib/generated/intl/messages_ko.dart new file mode 100644 index 00000000..db209d1f --- /dev/null +++ b/lib/generated/intl/messages_ko.dart @@ -0,0 +1,84 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a ko locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'ko'; + + static m0(input) => "도달한 레벨 %"; + + static m1(input) => "${input} 손가락"; + + static m2(input) => "Hit Notes 을 ${input} 해봐요!"; + + static m3(input) => "가입한 ${input}"; + + static m4(input) => "레벨 ${input}"; + + static m5(input) => "사용 ${input}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static _notInlinedMessages(_) => { + "acoustic_guitar" : MessageLookupByLibrary.simpleMessage("어커스틱 기타"), + "classic" : MessageLookupByLibrary.simpleMessage("클래식 음악"), + "dark" : MessageLookupByLibrary.simpleMessage("어둠"), + "electric_guitar" : MessageLookupByLibrary.simpleMessage("전자 기타"), + "folk" : MessageLookupByLibrary.simpleMessage("민속 음악"), + "kpop" : MessageLookupByLibrary.simpleMessage("케이팝"), + "light" : MessageLookupByLibrary.simpleMessage("라이트"), + "other" : MessageLookupByLibrary.simpleMessage("다른 장르"), + "piano" : MessageLookupByLibrary.simpleMessage("피아노"), + "pop" : MessageLookupByLibrary.simpleMessage("팝"), + "system" : MessageLookupByLibrary.simpleMessage("시스템 기본값"), + "txt_about_invite" : MessageLookupByLibrary.simpleMessage("초대하다"), + "txt_about_rate" : MessageLookupByLibrary.simpleMessage("평가해주세요"), + "txt_all_ok" : MessageLookupByLibrary.simpleMessage("알겠어요"), + "txt_all_songs" : MessageLookupByLibrary.simpleMessage("모든 노래"), + "txt_button_feedback" : MessageLookupByLibrary.simpleMessage("피드백 해주세요"), + "txt_button_quit" : MessageLookupByLibrary.simpleMessage("그만하다."), + "txt_button_restart" : MessageLookupByLibrary.simpleMessage("다시 플레이"), + "txt_button_sign_in_facebook" : MessageLookupByLibrary.simpleMessage("페북으로 로그인하세요"), + "txt_button_sign_in_google" : MessageLookupByLibrary.simpleMessage("구글로 로그인 하세요"), + "txt_configure" : MessageLookupByLibrary.simpleMessage("구성"), + "txt_dialog_level_up_description" : m0, + "txt_dialog_loading_sound_description" : MessageLookupByLibrary.simpleMessage("사운드 로딩중…"), + "txt_difficult" : MessageLookupByLibrary.simpleMessage("어려움"), + "txt_difficulty" : MessageLookupByLibrary.simpleMessage("난이도"), + "txt_dynamic_link_invite_subject" : MessageLookupByLibrary.simpleMessage("히트틱이 초대했어요"), + "txt_easy" : MessageLookupByLibrary.simpleMessage("쉬운"), + "txt_fast" : MessageLookupByLibrary.simpleMessage("빠른"), + "txt_fingers" : m1, + "txt_game_button_continue" : MessageLookupByLibrary.simpleMessage("계속하기"), + "txt_game_complete_loading_gift" : MessageLookupByLibrary.simpleMessage("보상받기…"), + "txt_instrument_title_instruments" : MessageLookupByLibrary.simpleMessage("악기"), + "txt_invite_description" : m2, + "txt_joined" : m3, + "txt_level" : m4, + "txt_medium" : MessageLookupByLibrary.simpleMessage("미디엄"), + "txt_normal" : MessageLookupByLibrary.simpleMessage("일반"), + "txt_page_title_account" : MessageLookupByLibrary.simpleMessage("계정"), + "txt_select_language" : MessageLookupByLibrary.simpleMessage("언어 선택"), + "txt_slow" : MessageLookupByLibrary.simpleMessage("느리게"), + "txt_speed" : MessageLookupByLibrary.simpleMessage("속도"), + "txt_start" : MessageLookupByLibrary.simpleMessage("시작"), + "txt_theme" : MessageLookupByLibrary.simpleMessage("테마"), + "txt_too_early" : MessageLookupByLibrary.simpleMessage("너무 일찍"), + "txt_too_late" : MessageLookupByLibrary.simpleMessage("너무 늦음"), + "txt_too_many_fingers" : MessageLookupByLibrary.simpleMessage("손가락이 너무 많음"), + "txt_using" : m5 + }; +} diff --git a/lib/generated/intl/messages_vi.dart b/lib/generated/intl/messages_vi.dart new file mode 100644 index 00000000..a3ecaa4b --- /dev/null +++ b/lib/generated/intl/messages_vi.dart @@ -0,0 +1,84 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a vi locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'vi'; + + static m0(input) => "Bạn đã đạt cấp ${input}"; + + static m1(input) => "${input} ngón tay"; + + static m2(input) => "Xin chào, hãy chơi Hit Notes ${input}"; + + static m3(input) => "Đã tham gia ${input}"; + + static m4(input) => "Cấp ${input}"; + + static m5(input) => "Sử dụng ${input}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static _notInlinedMessages(_) => { + "acoustic_guitar" : MessageLookupByLibrary.simpleMessage("Guitar acoustic"), + "classic" : MessageLookupByLibrary.simpleMessage("Nhạc cổ điển"), + "dark" : MessageLookupByLibrary.simpleMessage("Tối"), + "electric_guitar" : MessageLookupByLibrary.simpleMessage("Guitar điện"), + "folk" : MessageLookupByLibrary.simpleMessage("Nhạc dân gian"), + "kpop" : MessageLookupByLibrary.simpleMessage("Kpop"), + "light" : MessageLookupByLibrary.simpleMessage("Sáng"), + "other" : MessageLookupByLibrary.simpleMessage("Thể loại khác"), + "piano" : MessageLookupByLibrary.simpleMessage("Piano"), + "pop" : MessageLookupByLibrary.simpleMessage("Nhạc pop"), + "system" : MessageLookupByLibrary.simpleMessage("Mặc định hệ thống"), + "txt_about_invite" : MessageLookupByLibrary.simpleMessage("Mời bạn bè"), + "txt_about_rate" : MessageLookupByLibrary.simpleMessage("Đánh giá"), + "txt_all_ok" : MessageLookupByLibrary.simpleMessage("Đồng ý"), + "txt_all_songs" : MessageLookupByLibrary.simpleMessage("Thư viện"), + "txt_button_feedback" : MessageLookupByLibrary.simpleMessage("Phản hồi cho chúng tôi"), + "txt_button_quit" : MessageLookupByLibrary.simpleMessage("Thoát"), + "txt_button_restart" : MessageLookupByLibrary.simpleMessage("Chơi lại"), + "txt_button_sign_in_facebook" : MessageLookupByLibrary.simpleMessage("Đăng nhập với Facebook"), + "txt_button_sign_in_google" : MessageLookupByLibrary.simpleMessage("Đăng nhập với Google"), + "txt_configure" : MessageLookupByLibrary.simpleMessage("Cấu hình"), + "txt_dialog_level_up_description" : m0, + "txt_dialog_loading_sound_description" : MessageLookupByLibrary.simpleMessage("Đang tải âm thanh…"), + "txt_difficult" : MessageLookupByLibrary.simpleMessage("Khó"), + "txt_difficulty" : MessageLookupByLibrary.simpleMessage("Độ khó"), + "txt_dynamic_link_invite_subject" : MessageLookupByLibrary.simpleMessage("Lời mời chơi Hit Notes"), + "txt_easy" : MessageLookupByLibrary.simpleMessage("Dễ"), + "txt_fast" : MessageLookupByLibrary.simpleMessage("Nhanh"), + "txt_fingers" : m1, + "txt_game_button_continue" : MessageLookupByLibrary.simpleMessage("Tiếp tục"), + "txt_game_complete_loading_gift" : MessageLookupByLibrary.simpleMessage("Đang lấy phần thưởng…"), + "txt_instrument_title_instruments" : MessageLookupByLibrary.simpleMessage("Nhạc cụ"), + "txt_invite_description" : m2, + "txt_joined" : m3, + "txt_level" : m4, + "txt_medium" : MessageLookupByLibrary.simpleMessage("Trung bình"), + "txt_normal" : MessageLookupByLibrary.simpleMessage("Bình thường"), + "txt_page_title_account" : MessageLookupByLibrary.simpleMessage("Tài khoản"), + "txt_select_language" : MessageLookupByLibrary.simpleMessage("Chọn ngôn ngữ"), + "txt_slow" : MessageLookupByLibrary.simpleMessage("Chậm"), + "txt_speed" : MessageLookupByLibrary.simpleMessage("Tốc độ"), + "txt_start" : MessageLookupByLibrary.simpleMessage("Bắt đầu"), + "txt_theme" : MessageLookupByLibrary.simpleMessage("Chủ đề"), + "txt_too_early" : MessageLookupByLibrary.simpleMessage("Hơi sớm"), + "txt_too_late" : MessageLookupByLibrary.simpleMessage("Hơi muộn"), + "txt_too_many_fingers" : MessageLookupByLibrary.simpleMessage("Quá nhiều ngón tay"), + "txt_using" : m5 + }; +} diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart new file mode 100644 index 00000000..e3e4480d --- /dev/null +++ b/lib/generated/intl/messages_zh.dart @@ -0,0 +1,84 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a zh locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'zh'; + + static m0(input) => "你已经得到几级了"; + + static m1(input) => "${input} 手指"; + + static m2(input) => "你好,来玩hittick ${input}"; + + static m3(input) => "已加入 ${input}"; + + static m4(input) => "级 ${input}"; + + static m5(input) => "使用 ${input}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static _notInlinedMessages(_) => { + "acoustic_guitar" : MessageLookupByLibrary.simpleMessage("Acoustic 吉他琴"), + "classic" : MessageLookupByLibrary.simpleMessage("经典音乐"), + "dark" : MessageLookupByLibrary.simpleMessage("黑暗"), + "electric_guitar" : MessageLookupByLibrary.simpleMessage("电吉他琴"), + "folk" : MessageLookupByLibrary.simpleMessage("民间音乐"), + "kpop" : MessageLookupByLibrary.simpleMessage("Kpop"), + "light" : MessageLookupByLibrary.simpleMessage("轻"), + "other" : MessageLookupByLibrary.simpleMessage("其他类型"), + "piano" : MessageLookupByLibrary.simpleMessage("钢琴"), + "pop" : MessageLookupByLibrary.simpleMessage("流行的"), + "system" : MessageLookupByLibrary.simpleMessage("系统默认"), + "txt_about_invite" : MessageLookupByLibrary.simpleMessage("邀请朋友"), + "txt_about_rate" : MessageLookupByLibrary.simpleMessage("评估"), + "txt_all_ok" : MessageLookupByLibrary.simpleMessage("同意"), + "txt_all_songs" : MessageLookupByLibrary.simpleMessage("图书馆"), + "txt_button_feedback" : MessageLookupByLibrary.simpleMessage("给我们意见"), + "txt_button_quit" : MessageLookupByLibrary.simpleMessage("退出"), + "txt_button_restart" : MessageLookupByLibrary.simpleMessage("再玩一次"), + "txt_button_sign_in_facebook" : MessageLookupByLibrary.simpleMessage("在facebook 登录"), + "txt_button_sign_in_google" : MessageLookupByLibrary.simpleMessage("在google登录"), + "txt_configure" : MessageLookupByLibrary.simpleMessage("配置"), + "txt_dialog_level_up_description" : m0, + "txt_dialog_loading_sound_description" : MessageLookupByLibrary.simpleMessage("在下载声音…"), + "txt_difficult" : MessageLookupByLibrary.simpleMessage("困难"), + "txt_difficulty" : MessageLookupByLibrary.simpleMessage("困难"), + "txt_dynamic_link_invite_subject" : MessageLookupByLibrary.simpleMessage("邀请玩hittick"), + "txt_easy" : MessageLookupByLibrary.simpleMessage("简单"), + "txt_fast" : MessageLookupByLibrary.simpleMessage("快"), + "txt_fingers" : m1, + "txt_game_button_continue" : MessageLookupByLibrary.simpleMessage("继续"), + "txt_game_complete_loading_gift" : MessageLookupByLibrary.simpleMessage("在拿礼物…"), + "txt_instrument_title_instruments" : MessageLookupByLibrary.simpleMessage("乐器"), + "txt_invite_description" : m2, + "txt_joined" : m3, + "txt_level" : m4, + "txt_medium" : MessageLookupByLibrary.simpleMessage("中"), + "txt_normal" : MessageLookupByLibrary.simpleMessage("普通"), + "txt_page_title_account" : MessageLookupByLibrary.simpleMessage("账户"), + "txt_select_language" : MessageLookupByLibrary.simpleMessage("选语言"), + "txt_slow" : MessageLookupByLibrary.simpleMessage("慢"), + "txt_speed" : MessageLookupByLibrary.simpleMessage("速度"), + "txt_start" : MessageLookupByLibrary.simpleMessage("开始"), + "txt_theme" : MessageLookupByLibrary.simpleMessage("主题"), + "txt_too_early" : MessageLookupByLibrary.simpleMessage("太早了"), + "txt_too_late" : MessageLookupByLibrary.simpleMessage("太晚了"), + "txt_too_many_fingers" : MessageLookupByLibrary.simpleMessage("手指太多"), + "txt_using" : m5 + }; +} diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart new file mode 100644 index 00000000..47dfbf6c --- /dev/null +++ b/lib/generated/l10n.dart @@ -0,0 +1,538 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'intl/messages_all.dart'; + +// ************************************************************************** +// Generator: Flutter Intl IDE plugin +// Made by Localizely +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values + +class S { + S(); + + static S current; + + static const AppLocalizationDelegate delegate = + AppLocalizationDelegate(); + + static Future load(Locale locale) { + final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); + return initializeMessages(localeName).then((_) { + Intl.defaultLocale = localeName; + S.current = S(); + + return S.current; + }); + } + + static S of(BuildContext context) { + return Localizations.of(context, S); + } + + /// `Acoustic Guitar` + String get acoustic_guitar { + return Intl.message( + 'Acoustic Guitar', + name: 'acoustic_guitar', + desc: '', + args: [], + ); + } + + /// `Classic` + String get classic { + return Intl.message( + 'Classic', + name: 'classic', + desc: '', + args: [], + ); + } + + /// `Dark` + String get dark { + return Intl.message( + 'Dark', + name: 'dark', + desc: '', + args: [], + ); + } + + /// `Electric Guitar` + String get electric_guitar { + return Intl.message( + 'Electric Guitar', + name: 'electric_guitar', + desc: '', + args: [], + ); + } + + /// `Folk` + String get folk { + return Intl.message( + 'Folk', + name: 'folk', + desc: '', + args: [], + ); + } + + /// `Kpop` + String get kpop { + return Intl.message( + 'Kpop', + name: 'kpop', + desc: '', + args: [], + ); + } + + /// `Light` + String get light { + return Intl.message( + 'Light', + name: 'light', + desc: '', + args: [], + ); + } + + /// `Other genre` + String get other { + return Intl.message( + 'Other genre', + name: 'other', + desc: '', + args: [], + ); + } + + /// `Piano` + String get piano { + return Intl.message( + 'Piano', + name: 'piano', + desc: '', + args: [], + ); + } + + /// `Pop` + String get pop { + return Intl.message( + 'Pop', + name: 'pop', + desc: '', + args: [], + ); + } + + /// `System default` + String get system { + return Intl.message( + 'System default', + name: 'system', + desc: '', + args: [], + ); + } + + /// `Invite` + String get txt_about_invite { + return Intl.message( + 'Invite', + name: 'txt_about_invite', + desc: '', + args: [], + ); + } + + /// `Rate us` + String get txt_about_rate { + return Intl.message( + 'Rate us', + name: 'txt_about_rate', + desc: '', + args: [], + ); + } + + /// `Ok` + String get txt_all_ok { + return Intl.message( + 'Ok', + name: 'txt_all_ok', + desc: '', + args: [], + ); + } + + /// `All songs` + String get txt_all_songs { + return Intl.message( + 'All songs', + name: 'txt_all_songs', + desc: '', + args: [], + ); + } + + /// `Feedback us` + String get txt_button_feedback { + return Intl.message( + 'Feedback us', + name: 'txt_button_feedback', + desc: '', + args: [], + ); + } + + /// `Quit` + String get txt_button_quit { + return Intl.message( + 'Quit', + name: 'txt_button_quit', + desc: '', + args: [], + ); + } + + /// `Play again` + String get txt_button_restart { + return Intl.message( + 'Play again', + name: 'txt_button_restart', + desc: '', + args: [], + ); + } + + /// `Sign in with Facebook` + String get txt_button_sign_in_facebook { + return Intl.message( + 'Sign in with Facebook', + name: 'txt_button_sign_in_facebook', + desc: '', + args: [], + ); + } + + /// `Sign in with Google` + String get txt_button_sign_in_google { + return Intl.message( + 'Sign in with Google', + name: 'txt_button_sign_in_google', + desc: '', + args: [], + ); + } + + /// `Configure` + String get txt_configure { + return Intl.message( + 'Configure', + name: 'txt_configure', + desc: '', + args: [], + ); + } + + /// `Reached level {input}` + String txt_dialog_level_up_description(Object input) { + return Intl.message( + 'Reached level $input', + name: 'txt_dialog_level_up_description', + desc: '', + args: [input], + ); + } + + /// `Loading sound…` + String get txt_dialog_loading_sound_description { + return Intl.message( + 'Loading sound…', + name: 'txt_dialog_loading_sound_description', + desc: '', + args: [], + ); + } + + /// `Difficult` + String get txt_difficult { + return Intl.message( + 'Difficult', + name: 'txt_difficult', + desc: '', + args: [], + ); + } + + /// `Difficulty` + String get txt_difficulty { + return Intl.message( + 'Difficulty', + name: 'txt_difficulty', + desc: '', + args: [], + ); + } + + /// `Hit Notes invite` + String get txt_dynamic_link_invite_subject { + return Intl.message( + 'Hit Notes invite', + name: 'txt_dynamic_link_invite_subject', + desc: '', + args: [], + ); + } + + /// `Easy` + String get txt_easy { + return Intl.message( + 'Easy', + name: 'txt_easy', + desc: '', + args: [], + ); + } + + /// `Fast` + String get txt_fast { + return Intl.message( + 'Fast', + name: 'txt_fast', + desc: '', + args: [], + ); + } + + /// `{input} fingers` + String txt_fingers(Object input) { + return Intl.message( + '$input fingers', + name: 'txt_fingers', + desc: '', + args: [input], + ); + } + + /// `Continue` + String get txt_game_button_continue { + return Intl.message( + 'Continue', + name: 'txt_game_button_continue', + desc: '', + args: [], + ); + } + + /// `Getting reward…` + String get txt_game_complete_loading_gift { + return Intl.message( + 'Getting reward…', + name: 'txt_game_complete_loading_gift', + desc: '', + args: [], + ); + } + + /// `Instruments` + String get txt_instrument_title_instruments { + return Intl.message( + 'Instruments', + name: 'txt_instrument_title_instruments', + desc: '', + args: [], + ); + } + + /// `Hey, let play Hit Notes {input}` + String txt_invite_description(Object input) { + return Intl.message( + 'Hey, let play Hit Notes $input', + name: 'txt_invite_description', + desc: '', + args: [input], + ); + } + + /// `Joined {input}` + String txt_joined(Object input) { + return Intl.message( + 'Joined $input', + name: 'txt_joined', + desc: '', + args: [input], + ); + } + + /// `Level {input}` + String txt_level(Object input) { + return Intl.message( + 'Level $input', + name: 'txt_level', + desc: '', + args: [input], + ); + } + + /// `Medium` + String get txt_medium { + return Intl.message( + 'Medium', + name: 'txt_medium', + desc: '', + args: [], + ); + } + + /// `Normal` + String get txt_normal { + return Intl.message( + 'Normal', + name: 'txt_normal', + desc: '', + args: [], + ); + } + + /// `Account` + String get txt_page_title_account { + return Intl.message( + 'Account', + name: 'txt_page_title_account', + desc: '', + args: [], + ); + } + + /// `Select language` + String get txt_select_language { + return Intl.message( + 'Select language', + name: 'txt_select_language', + desc: '', + args: [], + ); + } + + /// `Slow` + String get txt_slow { + return Intl.message( + 'Slow', + name: 'txt_slow', + desc: '', + args: [], + ); + } + + /// `Speed` + String get txt_speed { + return Intl.message( + 'Speed', + name: 'txt_speed', + desc: '', + args: [], + ); + } + + /// `Start` + String get txt_start { + return Intl.message( + 'Start', + name: 'txt_start', + desc: '', + args: [], + ); + } + + /// `Theme` + String get txt_theme { + return Intl.message( + 'Theme', + name: 'txt_theme', + desc: '', + args: [], + ); + } + + /// `Too early` + String get txt_too_early { + return Intl.message( + 'Too early', + name: 'txt_too_early', + desc: '', + args: [], + ); + } + + /// `Too late` + String get txt_too_late { + return Intl.message( + 'Too late', + name: 'txt_too_late', + desc: '', + args: [], + ); + } + + /// `Too many fingers` + String get txt_too_many_fingers { + return Intl.message( + 'Too many fingers', + name: 'txt_too_many_fingers', + desc: '', + args: [], + ); + } + + /// `Using {input}` + String txt_using(Object input) { + return Intl.message( + 'Using $input', + name: 'txt_using', + desc: '', + args: [input], + ); + } +} + +class AppLocalizationDelegate extends LocalizationsDelegate { + const AppLocalizationDelegate(); + + List get supportedLocales { + return const [ + Locale.fromSubtags(languageCode: 'en'), + Locale.fromSubtags(languageCode: 'ko'), + Locale.fromSubtags(languageCode: 'vi'), + Locale.fromSubtags(languageCode: 'zh'), + ]; + } + + @override + bool isSupported(Locale locale) => _isSupported(locale); + @override + Future load(Locale locale) => S.load(locale); + @override + bool shouldReload(AppLocalizationDelegate old) => false; + + bool _isSupported(Locale locale) { + if (locale != null) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/lib/home/home_bloc.dart b/lib/home/home_bloc.dart new file mode 100644 index 00000000..270209f3 --- /dev/null +++ b/lib/home/home_bloc.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; + +import '../instrument/instruments_repository.dart'; +import '../midi_processor.dart'; +import '../user/user_repository.dart'; +import 'home_event.dart'; +import 'home_state.dart'; + +class HomeBloc extends Bloc { + final UserRepository _userRepository; + final InstrumentsRepository _instrumentsRepository; + + HomeBloc(this._userRepository, this._instrumentsRepository) + : super(HomeInitial()) { + _userRepository.getCurrentUser().listen((user) async { + if (state is HomeInitial) { + final instrument = (await _instrumentsRepository.instruments()) + .firstWhere( + (instrument) => instrument.id == user.user.instrumentId); + MidiProcessor.getInstance().onSelectInstrument(instrument); + } else if (state is HomeUpdated) { + final oldUser = (state as HomeUpdated).user; + if (oldUser.user.instrumentId != user.user.instrumentId) { + final instrument = (await _instrumentsRepository.instruments()) + .firstWhere( + (instrument) => instrument.id == user.user.instrumentId); + MidiProcessor.getInstance().onSelectInstrument(instrument); + } + } + add(HomeUpdate(user)); + }); + } + + @override + Stream mapEventToState(HomeEvent event) async* { + if (event is HomeUpdate) { + yield HomeUpdated(event.user); + } + } +} diff --git a/lib/home/home_event.dart b/lib/home/home_event.dart new file mode 100644 index 00000000..a740bbaa --- /dev/null +++ b/lib/home/home_event.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +import '../user/user_repository.dart'; + +abstract class HomeEvent extends Equatable { + const HomeEvent(); +} + +class HomeUpdate extends HomeEvent { + final AppUser user; + + const HomeUpdate(this.user); + + @override + List get props => [user]; +} diff --git a/lib/home/home_state.dart b/lib/home/home_state.dart new file mode 100644 index 00000000..2a24aa20 --- /dev/null +++ b/lib/home/home_state.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; + +import '../user/user_repository.dart'; + +abstract class HomeState extends Equatable { + const HomeState(); + + @override + List get props => []; +} + +class HomeInitial extends HomeState {} + +class HomeUpdated extends HomeState { + final AppUser user; + + const HomeUpdated(this.user); + + @override + List get props => [user]; + + @override + String toString() => 'HomeUpdated { photoUrl: $user }'; +} diff --git a/lib/home/home_widget.dart b/lib/home/home_widget.dart new file mode 100644 index 00000000..e548a96b --- /dev/null +++ b/lib/home/home_widget.dart @@ -0,0 +1,105 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; + +import '../extra_actions.dart'; +import '../generated/l10n.dart'; +import '../main.dart'; +import '../routes.dart'; +import '../search/search_widget.dart'; +import '../songs/songs_widget.dart'; +import '../util.dart'; +import 'home_bloc.dart'; +import 'home_state.dart'; + +class HomeWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Scaffold( + body: DefaultTabController( + length: songTags.length, + child: NestedScrollView( + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + title: Text(S.of(context).txt_all_songs, + style: Theme.of(context) + .appBarTheme + .textTheme + .headline5), + elevation: 2, + floating: true, + pinned: true, + snap: true, + forceElevated: true, + actions: [ + IconButton( + icon: Icon(Icons.search_rounded), + onPressed: () { + showSearch( + context: context, delegate: SearchWidget()); + }, + ), + IconButton( + icon: Image( + image: AssetImage('assets/images/img_guitar.png'), + color: + Theme.of(context).appBarTheme.iconTheme.color, + ), + onPressed: () async { + await Navigator.pushNamed( + context, Routes.instrument); + }), + IconButton( + icon: ClipOval( + child: (state is HomeUpdated && + !state.user.isAnonymous) + ? CachedNetworkImage( + imageUrl: state.user.photoUrl, + placeholder: (context, url) => + Icon(Icons.account_circle_rounded), + memCacheWidth: 24.toPixel(), + memCacheHeight: 24.toPixel()) + : Icon(Icons.account_circle_rounded), + ), + onPressed: () async { + await Navigator.pushNamed( + context, Routes.account); + }), + ExtraActions() + ], + bottom: TabBar( + isScrollable: true, + tabs: songTags + .map((tabName) => Tab( + text: Intl.message( + '', + /* FIXME Localization name of instrument should be taken from server, not from local text resources */ + name: tabName, + desc: '', + args: [], + ), + )) + .toList(), + )), + ]; + }, + body: TabBarView( + children: songTags + .asMap() + .map((index, tabName) => + MapEntry(index, SongsWidget(tagNumber: index))) + .values + .toList(), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/instrument/instrument.dart b/lib/instrument/instrument.dart new file mode 100644 index 00000000..3ce3a504 --- /dev/null +++ b/lib/instrument/instrument.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'pitch_note.dart'; + +part 'instrument.g.dart'; + +@JsonSerializable(explicitToJson: true) +class Instrument { + String id; + Map soundFiles; + Map soundNotes; + int minNote; + int maxNote; + double volume; + + Instrument(this.id, this.soundFiles, this.soundNotes, this.minNote, + this.maxNote, this.volume); + + factory Instrument.fromJson(Map json) => + _$InstrumentFromJson(json); + + Map toJson() => _$InstrumentToJson(this); +} diff --git a/lib/instrument/instrument.g.dart b/lib/instrument/instrument.g.dart new file mode 100644 index 00000000..0db8affe --- /dev/null +++ b/lib/instrument/instrument.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'instrument.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Instrument _$InstrumentFromJson(Map json) { + return Instrument( + json['id'] as String, + (json['soundFiles'] as Map)?.map( + (k, e) => MapEntry(int.parse(k), e as String), + ), + (json['soundNotes'] as Map)?.map( + (k, e) => MapEntry(int.parse(k), + e == null ? null : PitchNote.fromJson(e as Map)), + ), + json['minNote'] as int, + json['maxNote'] as int, + (json['volume'] as num)?.toDouble(), + ); +} + +Map _$InstrumentToJson(Instrument instance) => + { + 'id': instance.id, + 'soundFiles': + instance.soundFiles?.map((k, e) => MapEntry(k.toString(), e)), + 'soundNotes': instance.soundNotes + ?.map((k, e) => MapEntry(k.toString(), e?.toJson())), + 'minNote': instance.minNote, + 'maxNote': instance.maxNote, + 'volume': instance.volume, + }; diff --git a/lib/instrument/instruments_repository.dart b/lib/instrument/instruments_repository.dart new file mode 100644 index 00000000..673e77a7 --- /dev/null +++ b/lib/instrument/instruments_repository.dart @@ -0,0 +1,7 @@ +import 'dart:async'; + +import 'instrument.dart'; + +abstract class InstrumentsRepository { + Future> instruments(); +} diff --git a/lib/instrument/instruments_repository_impl.dart b/lib/instrument/instruments_repository_impl.dart new file mode 100644 index 00000000..1f7dcc27 --- /dev/null +++ b/lib/instrument/instruments_repository_impl.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; + +import 'instrument.dart'; +import 'instruments_repository.dart'; + +class InstrumentsRepositoryImpl implements InstrumentsRepository { + @override + Future> instruments() async { + return (await FirebaseFirestore.instance.collection('instruments').get()) + .docs + .map((e) => Instrument.fromJson(e.data())) + .toList(); + } +} diff --git a/lib/instrument/instruments_widget.dart b/lib/instrument/instruments_widget.dart new file mode 100644 index 00000000..2e38bfbc --- /dev/null +++ b/lib/instrument/instruments_widget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; + +import '../generated/l10n.dart'; +import '../loading_widget.dart'; +import '../user/user_bloc.dart'; + +class InstrumentsWidget extends StatelessWidget { + InstrumentsWidget({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).txt_instrument_title_instruments, + style: Theme.of(context).appBarTheme.textTheme.headline5)), + body: (() { + if (state is UserLoading) { + return LoadingWidget(); + } else if (state is UserUpdated) { + final instruments = state.instruments; + return ListView.builder( + itemCount: instruments.length, + itemBuilder: (context, index) { + final instrument = instruments[index]; + return RadioListTile( + title: Text( + Intl.message( + '', + /* FIXME Localization name of instrument should be taken from server, not from local text resources */ + name: instrument.id, + desc: '', + args: [], + ), + style: Theme.of(context).textTheme.headline6), + value: instrument.id, + groupValue: state.user.user.instrumentId, + onChanged: (String value) { + BlocProvider.of(context) + .add(ChangeInstrument(value)); + }, + ); + }, + ); + } else { + return Container(); + } + }()), + ); + }, + ); + } +} diff --git a/lib/instrument/pitch_note.dart b/lib/instrument/pitch_note.dart new file mode 100644 index 00000000..b01494b1 --- /dev/null +++ b/lib/instrument/pitch_note.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'pitch_note.g.dart'; + +@JsonSerializable(explicitToJson: true) +class PitchNote { + int note; + double pitch; + + PitchNote(this.note, this.pitch); + + factory PitchNote.fromJson(Map json) => + _$PitchNoteFromJson(json); + + Map toJson() => _$PitchNoteToJson(this); +} diff --git a/lib/instrument/pitch_note.g.dart b/lib/instrument/pitch_note.g.dart new file mode 100644 index 00000000..58c57711 --- /dev/null +++ b/lib/instrument/pitch_note.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pitch_note.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PitchNote _$PitchNoteFromJson(Map json) { + return PitchNote( + json['note'] as int, + (json['pitch'] as num)?.toDouble(), + ); +} + +Map _$PitchNoteToJson(PitchNote instance) => { + 'note': instance.note, + 'pitch': instance.pitch, + }; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb new file mode 100644 index 00000000..336de9a4 --- /dev/null +++ b/lib/l10n/intl_en.arb @@ -0,0 +1,97 @@ +{ + "@@locale": "en", + "acoustic_guitar": "Acoustic Guitar", + "@acoustic_guitar": {}, + "classic": "Classic", + "@classic": {}, + "dark": "Dark", + "@dark": {}, + "electric_guitar": "Electric Guitar", + "@electric_guitar": {}, + "folk": "Folk", + "@folk": {}, + "kpop": "Kpop", + "@kpop": {}, + "light": "Light", + "@light": {}, + "other": "Other genre", + "@other": {}, + "piano": "Piano", + "@piano": {}, + "pop": "Pop", + "@pop": {}, + "system": "System default", + "@system": {}, + "txt_about_invite": "Invite", + "@txt_about_invite": {}, + "txt_about_rate": "Rate us", + "@txt_about_rate": {}, + "txt_all_ok": "Ok", + "@txt_all_ok": {}, + "txt_all_songs": "All songs", + "@txt_all_songs": {}, + "txt_button_feedback": "Feedback us", + "@txt_button_feedback": {}, + "txt_button_quit": "Quit", + "@txt_button_quit": {}, + "txt_button_restart": "Play again", + "@txt_button_restart": {}, + "txt_button_sign_in_facebook": "Sign in with Facebook", + "@txt_button_sign_in_facebook": {}, + "txt_button_sign_in_google": "Sign in with Google", + "@txt_button_sign_in_google": {}, + "txt_configure": "Configure", + "@txt_configure": {}, + "txt_dialog_level_up_description": "Reached level {input}", + "@txt_dialog_level_up_description": {}, + "txt_dialog_loading_sound_description": "Loading sound…", + "@txt_dialog_loading_sound_description": {}, + "txt_difficult": "Difficult", + "@txt_difficult": {}, + "txt_difficulty": "Difficulty", + "@txt_difficulty": {}, + "txt_dynamic_link_invite_subject": "Hit Notes invite", + "@txt_dynamic_link_invite_subject": {}, + "txt_easy": "Easy", + "@txt_easy": {}, + "txt_fast": "Fast", + "@txt_fast": {}, + "txt_fingers": "{input} fingers", + "@txt_fingers": {}, + "txt_game_button_continue": "Continue", + "@txt_game_button_continue": {}, + "txt_game_complete_loading_gift": "Getting reward…", + "@txt_game_complete_loading_gift": {}, + "txt_instrument_title_instruments": "Instruments", + "@txt_instrument_title_instruments": {}, + "txt_invite_description": "Hey, let play Hit Notes {input}", + "@txt_invite_description": {}, + "txt_joined": "Joined {input}", + "@txt_joined": {}, + "txt_level": "Level {input}", + "@txt_level": {}, + "txt_medium": "Medium", + "@txt_medium": {}, + "txt_normal": "Normal", + "@txt_normal": {}, + "txt_page_title_account": "Account", + "@txt_page_title_account": {}, + "txt_select_language": "Select language", + "@txt_select_language": {}, + "txt_slow": "Slow", + "@txt_slow": {}, + "txt_speed": "Speed", + "@txt_speed": {}, + "txt_start": "Start", + "@txt_start": {}, + "txt_theme": "Theme", + "@txt_theme": {}, + "txt_too_early": "Too early", + "@txt_too_early": {}, + "txt_too_late": "Too late", + "@txt_too_late": {}, + "txt_too_many_fingers": "Too many fingers", + "@txt_too_many_fingers": {}, + "txt_using": "Using {input}", + "@txt_using": {} +} \ No newline at end of file diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb new file mode 100644 index 00000000..c260bdba --- /dev/null +++ b/lib/l10n/intl_ko.arb @@ -0,0 +1,97 @@ +{ + "@@locale": "ko", + "acoustic_guitar": "어커스틱 기타", + "@acoustic_guitar": {}, + "classic": "클래식 음악", + "@classic": {}, + "dark": "어둠", + "@dark": {}, + "electric_guitar": "전자 기타", + "@electric_guitar": {}, + "folk": "민속 음악", + "@folk": {}, + "kpop": "케이팝", + "@kpop": {}, + "light": "라이트", + "@light": {}, + "other": "다른 장르", + "@other": {}, + "piano": "피아노", + "@piano": {}, + "pop": "팝", + "@pop": {}, + "system": "시스템 기본값", + "@system": {}, + "txt_about_invite": "초대하다", + "@txt_about_invite": {}, + "txt_about_rate": "평가해주세요", + "@txt_about_rate": {}, + "txt_all_ok": "알겠어요", + "@txt_all_ok": {}, + "txt_all_songs": "모든 노래", + "@txt_all_songs": {}, + "txt_button_feedback": "피드백 해주세요", + "@txt_button_feedback": {}, + "txt_button_quit": "그만하다.", + "@txt_button_quit": {}, + "txt_button_restart": "다시 플레이", + "@txt_button_restart": {}, + "txt_button_sign_in_facebook": "페북으로 로그인하세요", + "@txt_button_sign_in_facebook": {}, + "txt_button_sign_in_google": "구글로 로그인 하세요", + "@txt_button_sign_in_google": {}, + "txt_configure": "구성", + "@txt_configure": {}, + "txt_dialog_level_up_description": "도달한 레벨 %", + "@txt_dialog_level_up_description": {}, + "txt_dialog_loading_sound_description": "사운드 로딩중…", + "@txt_dialog_loading_sound_description": {}, + "txt_difficult": "어려움", + "@txt_difficult": {}, + "txt_difficulty": "난이도", + "@txt_difficulty": {}, + "txt_dynamic_link_invite_subject": "히트틱이 초대했어요", + "@txt_dynamic_link_invite_subject": {}, + "txt_easy": "쉬운", + "@txt_easy": {}, + "txt_fast": "빠른", + "@txt_fast": {}, + "txt_fingers": "{input} 손가락", + "@txt_fingers": {}, + "txt_game_button_continue": "계속하기", + "@txt_game_button_continue": {}, + "txt_game_complete_loading_gift": "보상받기…", + "@txt_game_complete_loading_gift": {}, + "txt_instrument_title_instruments": "악기", + "@txt_instrument_title_instruments": {}, + "txt_invite_description": "Hit Notes 을 {input} 해봐요!", + "@txt_invite_description": {}, + "txt_joined": "가입한 {input}", + "@txt_joined": {}, + "txt_level": "레벨 {input}", + "@txt_level": {}, + "txt_medium": "미디엄", + "@txt_medium": {}, + "txt_normal": "일반", + "@txt_normal": {}, + "txt_page_title_account": "계정", + "@txt_page_title_account": {}, + "txt_select_language": "언어 선택", + "@txt_select_language": {}, + "txt_slow": "느리게", + "@txt_slow": {}, + "txt_speed": "속도", + "@txt_speed": {}, + "txt_start": "시작", + "@txt_start": {}, + "txt_theme": "테마", + "@txt_theme": {}, + "txt_too_early": "너무 일찍", + "@txt_too_early": {}, + "txt_too_late": "너무 늦음", + "@txt_too_late": {}, + "txt_too_many_fingers": "손가락이 너무 많음", + "@txt_too_many_fingers": {}, + "txt_using": "사용 {input}", + "@txt_using": {} +} \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb new file mode 100644 index 00000000..5010a702 --- /dev/null +++ b/lib/l10n/intl_vi.arb @@ -0,0 +1,97 @@ +{ + "@@locale": "vi", + "acoustic_guitar": "Guitar acoustic", + "@acoustic_guitar": {}, + "classic": "Nhạc cổ điển", + "@classic": {}, + "dark": "Tối", + "@dark": {}, + "electric_guitar": "Guitar điện", + "@electric_guitar": {}, + "folk": "Nhạc dân gian", + "@folk": {}, + "kpop": "Kpop", + "@kpop": {}, + "light": "Sáng", + "@light": {}, + "other": "Thể loại khác", + "@other": {}, + "piano": "Piano", + "@piano": {}, + "pop": "Nhạc pop", + "@pop": {}, + "system": "Mặc định hệ thống", + "@system": {}, + "txt_about_invite": "Mời bạn bè", + "@txt_about_invite": {}, + "txt_about_rate": "Đánh giá", + "@txt_about_rate": {}, + "txt_all_ok": "Đồng ý", + "@txt_all_ok": {}, + "txt_all_songs": "Thư viện", + "@txt_all_songs": {}, + "txt_button_feedback": "Phản hồi cho chúng tôi", + "@txt_button_feedback": {}, + "txt_button_quit": "Thoát", + "@txt_button_quit": {}, + "txt_button_restart": "Chơi lại", + "@txt_button_restart": {}, + "txt_button_sign_in_facebook": "Đăng nhập với Facebook", + "@txt_button_sign_in_facebook": {}, + "txt_button_sign_in_google": "Đăng nhập với Google", + "@txt_button_sign_in_google": {}, + "txt_configure": "Cấu hình", + "@txt_configure": {}, + "txt_dialog_level_up_description": "Bạn đã đạt cấp {input}", + "@txt_dialog_level_up_description": {}, + "txt_dialog_loading_sound_description": "Đang tải âm thanh…", + "@txt_dialog_loading_sound_description": {}, + "txt_difficult": "Khó", + "@txt_difficult": {}, + "txt_difficulty": "Độ khó", + "@txt_difficulty": {}, + "txt_dynamic_link_invite_subject": "Lời mời chơi Hit Notes", + "@txt_dynamic_link_invite_subject": {}, + "txt_easy": "Dễ", + "@txt_easy": {}, + "txt_fast": "Nhanh", + "@txt_fast": {}, + "txt_fingers": "{input} ngón tay", + "@txt_fingers": {}, + "txt_game_button_continue": "Tiếp tục", + "@txt_game_button_continue": {}, + "txt_game_complete_loading_gift": "Đang lấy phần thưởng…", + "@txt_game_complete_loading_gift": {}, + "txt_instrument_title_instruments": "Nhạc cụ", + "@txt_instrument_title_instruments": {}, + "txt_invite_description": "Xin chào, hãy chơi Hit Notes {input}", + "@txt_invite_description": {}, + "txt_joined": "Đã tham gia {input}", + "@txt_joined": {}, + "txt_level": "Cấp {input}", + "@txt_level": {}, + "txt_medium": "Trung bình", + "@txt_medium": {}, + "txt_normal": "Bình thường", + "@txt_normal": {}, + "txt_page_title_account": "Tài khoản", + "@txt_page_title_account": {}, + "txt_select_language": "Chọn ngôn ngữ", + "@txt_select_language": {}, + "txt_slow": "Chậm", + "@txt_slow": {}, + "txt_speed": "Tốc độ", + "@txt_speed": {}, + "txt_start": "Bắt đầu", + "@txt_start": {}, + "txt_theme": "Chủ đề", + "@txt_theme": {}, + "txt_too_early": "Hơi sớm", + "@txt_too_early": {}, + "txt_too_late": "Hơi muộn", + "@txt_too_late": {}, + "txt_too_many_fingers": "Quá nhiều ngón tay", + "@txt_too_many_fingers": {}, + "txt_using": "Sử dụng {input}", + "@txt_using": {} +} \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb new file mode 100644 index 00000000..ccd89189 --- /dev/null +++ b/lib/l10n/intl_zh.arb @@ -0,0 +1,97 @@ +{ + "@@locale": "zh", + "acoustic_guitar": "Acoustic 吉他琴", + "@acoustic_guitar": {}, + "classic": "经典音乐", + "@classic": {}, + "dark": "黑暗", + "@dark": {}, + "electric_guitar": "电吉他琴", + "@electric_guitar": {}, + "folk": "民间音乐", + "@folk": {}, + "kpop": "Kpop", + "@kpop": {}, + "light": "轻", + "@light": {}, + "other": "其他类型", + "@other": {}, + "piano": "钢琴", + "@piano": {}, + "pop": "流行的", + "@pop": {}, + "system": "系统默认", + "@system": {}, + "txt_about_invite": "邀请朋友", + "@txt_about_invite": {}, + "txt_about_rate": "评估", + "@txt_about_rate": {}, + "txt_all_ok": "同意", + "@txt_all_ok": {}, + "txt_all_songs": "图书馆", + "@txt_all_songs": {}, + "txt_button_feedback": "给我们意见", + "@txt_button_feedback": {}, + "txt_button_quit": "退出", + "@txt_button_quit": {}, + "txt_button_restart": "再玩一次", + "@txt_button_restart": {}, + "txt_button_sign_in_facebook": "在facebook 登录", + "@txt_button_sign_in_facebook": {}, + "txt_button_sign_in_google": "在google登录", + "@txt_button_sign_in_google": {}, + "txt_configure": "配置", + "@txt_configure": {}, + "txt_dialog_level_up_description": "你已经得到几级了", + "@txt_dialog_level_up_description": {}, + "txt_dialog_loading_sound_description": "在下载声音…", + "@txt_dialog_loading_sound_description": {}, + "txt_difficult": "困难", + "@txt_difficult": {}, + "txt_difficulty": "困难", + "@txt_difficulty": {}, + "txt_dynamic_link_invite_subject": "邀请玩hittick", + "@txt_dynamic_link_invite_subject": {}, + "txt_easy": "简单", + "@txt_easy": {}, + "txt_fast": "快", + "@txt_fast": {}, + "txt_fingers": "{input} 手指", + "@txt_fingers": {}, + "txt_game_button_continue": "继续", + "@txt_game_button_continue": {}, + "txt_game_complete_loading_gift": "在拿礼物…", + "@txt_game_complete_loading_gift": {}, + "txt_instrument_title_instruments": "乐器", + "@txt_instrument_title_instruments": {}, + "txt_invite_description": "你好,来玩hittick {input}", + "@txt_invite_description": {}, + "txt_joined": "已加入 {input}", + "@txt_joined": {}, + "txt_level": "级 {input}", + "@txt_level": {}, + "txt_medium": "中", + "@txt_medium": {}, + "txt_normal": "普通", + "@txt_normal": {}, + "txt_page_title_account": "账户", + "@txt_page_title_account": {}, + "txt_select_language": "选语言", + "@txt_select_language": {}, + "txt_slow": "慢", + "@txt_slow": {}, + "txt_speed": "速度", + "@txt_speed": {}, + "txt_start": "开始", + "@txt_start": {}, + "txt_theme": "主题", + "@txt_theme": {}, + "txt_too_early": "太早了", + "@txt_too_early": {}, + "txt_too_late": "太晚了", + "@txt_too_late": {}, + "txt_too_many_fingers": "手指太多", + "@txt_too_many_fingers": {}, + "txt_using": "使用 {input}", + "@txt_using": {} +} \ No newline at end of file diff --git a/lib/loading_widget.dart b/lib/loading_widget.dart new file mode 100644 index 00000000..1e8553f9 --- /dev/null +++ b/lib/loading_widget.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class LoadingWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: CircularProgressIndicator(), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 00000000..63b294ac --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +import 'authentication/authentication_bloc.dart'; +import 'game/game_bloc.dart'; +import 'game/game_widget.dart'; +import 'game_config/game_config_bloc.dart'; +import 'game_config/game_config_widget.dart'; +import 'generated/l10n.dart'; +import 'home/home_bloc.dart'; +import 'home/home_widget.dart'; +import 'instrument/instruments_repository_impl.dart'; +import 'instrument/instruments_widget.dart'; +import 'routes.dart'; +import 'setting/locale_widget.dart'; +import 'setting/setting_bloc.dart'; +import 'setting/theme_widget.dart'; +import 'simple_bloc_observer.dart'; +import 'songs/songs_bloc.dart'; +import 'songs/songs_event.dart'; +import 'songs/songs_repository.dart'; +import 'songs/songs_repository_impl.dart'; +import 'splash_widget.dart'; +import 'user/user_bloc.dart'; +import 'user/user_repository_impl.dart'; +import 'user/user_widget.dart'; + +const songTags = [ + 'pop', + 'classic', + 'folk', + 'kpop', + 'other', +]; + +Color primaryColor; +Color secondaryColor; +Color backgroundColor; +Color onBackgroundColor; +Paint paint; + +void main() async { + Bloc.observer = SimpleBlocObserver(); + runApp(App()); +} + +class App extends StatelessWidget { + @override + Widget build(BuildContext context) { + final userRepository = UserRepositoryImpl(); + final instrumentsRepository = InstrumentsRepositoryImpl(); + final songsRepository = SongsRepositoryImpl(); + SystemChrome.setEnabledSystemUIOverlays([]); + return MultiRepositoryProvider( + providers: [ + RepositoryProvider( + create: (context) => songsRepository), + ], + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + return SettingBloc(); + }, + ), + BlocProvider( + create: (context) { + return AuthenticationBloc(userRepository); + }, + ), + BlocProvider( + create: (context) { + return SongsBloc( + songsRepository: songsRepository, + )..add( + LoadMoreSongsByTagNumbers(songTags.asMap().keys.toList())); + }, + ), + BlocProvider( + create: (context) { + return UserBloc(instrumentsRepository, + userRepository: userRepository); + }, + ) + ], + child: + BlocBuilder(builder: (context, state) { + return MaterialApp( + title: 'Hit Notes', + debugShowCheckedModeBanner: false, + locale: state.locale, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + themeMode: state.themeMode, + theme: buildTheme(false), + darkTheme: buildTheme(true), + routes: { + Routes.splash: (context) { + return SplashWidget(); + }, + Routes.home: (context) { + return BlocProvider( + create: (context) => + HomeBloc(userRepository, instrumentsRepository), + child: HomeWidget()); + }, + Routes.gameConfig: (context) { + primaryColor = Theme.of(context).colorScheme.primary; + secondaryColor = Theme.of(context).colorScheme.secondary; + backgroundColor = Theme.of(context).colorScheme.background; + onBackgroundColor = + Theme.of(context).colorScheme.onBackground; + paint = Paint() + ..colorFilter = + ColorFilter.mode(primaryColor, BlendMode.srcIn); + return BlocProvider( + create: (context) => GameConfigBloc(), + child: GameConfigWidget()); + }, + Routes.game: (context) { + return BlocProvider( + create: (_) => GameBloc(), child: GameWidget()); + }, + Routes.account: (context) { + return UserWidget(); + }, + Routes.language: (context) { + return LocaleWidget(); + }, + Routes.theme: (context) { + return ThemeWidget(); + }, + Routes.instrument: (context) { + return InstrumentsWidget(); + }, + }, + ); + }), + )); + } + + ThemeData buildTheme(bool isDark) { + final primaryColor = Color(0xff4760e9); + final onPrimaryColor = Colors.white; + final secondaryColor = Color(0xfffd7c6e); + final backgroundColor = isDark ? Colors.black : Colors.white; + final onBackgroundColor = isDark ? Colors.white : Colors.black; + final screenHeadingTextStyle = + TextStyle(fontSize: 32.0, color: secondaryColor); + final screenTaskNameTextStyle = + TextStyle(fontSize: 20.0, color: onBackgroundColor); + final screenTaskDurationTextStyle = + TextStyle(fontSize: 16.0, color: onBackgroundColor); + final textTheme = TextTheme( + headline5: screenHeadingTextStyle, + bodyText2: screenTaskNameTextStyle, + bodyText1: screenTaskDurationTextStyle, + ); + return ThemeData( + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + primary: primaryColor, onPrimary: onPrimaryColor)), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + primary: onBackgroundColor, onSurface: onBackgroundColor)), + scaffoldBackgroundColor: backgroundColor, + tabBarTheme: TabBarTheme( + indicator: BoxDecoration( + border: Border( + bottom: BorderSide( + color: primaryColor, + width: 2, + ), + ), + ), + labelStyle: screenTaskNameTextStyle, + labelColor: primaryColor, + unselectedLabelColor: onBackgroundColor), + appBarTheme: AppBarTheme( + textTheme: textTheme, + color: backgroundColor, + iconTheme: IconThemeData(color: primaryColor), + ), + popupMenuTheme: PopupMenuThemeData(color: backgroundColor), + colorScheme: ColorScheme( + primary: primaryColor, + primaryVariant: primaryColor, + secondary: secondaryColor, + onPrimary: onPrimaryColor, + onError: onBackgroundColor, + error: backgroundColor, + onBackground: onBackgroundColor, + secondaryVariant: secondaryColor, + background: backgroundColor, + onSurface: onBackgroundColor, + onSecondary: secondaryColor, + brightness: isDark ? Brightness.dark : Brightness.light, + surface: backgroundColor, + ), + toggleableActiveColor: primaryColor, + iconTheme: IconThemeData( + color: primaryColor, + ), + textTheme: textTheme, + ); + } +} diff --git a/lib/midi_processor.dart b/lib/midi_processor.dart new file mode 100644 index 00000000..55ed526f --- /dev/null +++ b/lib/midi_processor.dart @@ -0,0 +1,97 @@ +import 'dart:async'; + +import 'package:flutter_cache_manager_firebase/flutter_cache_manager_firebase.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:soundpool/soundpool.dart'; + +import 'instrument/instrument.dart'; + +class MidiProcessor { + static MidiProcessor _instance; + + MidiProcessor._internal(); + + static MidiProcessor getInstance() { + _instance ??= MidiProcessor._internal(); + return _instance; + } + + Map> _noteToSoundIdAndPitches = {}; + Instrument _instrument; + + /* + Limit the number of simultaneous sounds, because, SoundPool only have 1MB heap size (media/libmediaplayerservice/MediaPlayerService.cpp), if exceeded, sound cannot be played. + AudioFlinger could not create track, status: -12 + SoundPool: Error creating AudioTrack + (Error -12 out of memory) + */ + static const _maxStreams = 8; + Soundpool _soundPool; + + final _activeSounds = {}; + + final StreamController _soundLoadedController = BehaviorSubject(); + + Stream get soundLoadedStream => _soundLoadedController.stream; + + void onSelectInstrument(Instrument instrument) { + if (_instrument != instrument) { + dispose(); + _soundPool = + Soundpool(streamType: StreamType.music, maxStreams: _maxStreams); + _instrument = instrument; + Future.wait(instrument.soundFiles.values + .map((e) => FirebaseCacheManager().getSingleFile(e))) + .then((files) => { + Future.wait(files.map((file) => _soundPool.loadPath(file.path))) + .then((soundIds) => { + _noteToSoundIdAndPitches = _instrument.soundNotes.map( + (note, pitchNote) => MapEntry( + note, + Pair( + soundIds[_instrument.soundFiles.keys + .toList() + .indexOf(pitchNote.note)], + pitchNote.pitch))), + if (soundIds.length == _instrument.soundFiles.length) + {_soundLoadedController.add(true)} + }) + }); + } + } + + Future playNote(int note) async { + print(note.toString()); + var pitchNote = note.toInt(); + while (pitchNote > _instrument.maxNote) { + pitchNote -= 12; + } + while (pitchNote < _instrument.minNote) { + pitchNote += 12; + } + final soundIdAndPitch = _noteToSoundIdAndPitches[pitchNote]; + if (soundIdAndPitch != null) { + _activeSounds.add(await _soundPool.play(soundIdAndPitch.first, + rate: soundIdAndPitch.second)); + if (_activeSounds.length == _maxStreams) { + final firstSound = _activeSounds.first; + await _soundPool.stop(firstSound); + _activeSounds.remove(firstSound); + } + } + } + + void dispose() { + _soundPool?.release(); + _soundLoadedController.add(false); + _activeSounds.clear(); + _noteToSoundIdAndPitches.clear(); + } +} + +class Pair { + final A first; + final B second; + + const Pair(this.first, this.second); +} diff --git a/lib/preferences.dart b/lib/preferences.dart new file mode 100644 index 00000000..62c3a265 --- /dev/null +++ b/lib/preferences.dart @@ -0,0 +1,29 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class Preferences { + static Preferences _instance; + static SharedPreferences _instance1; + + Preferences._internal(); + + static const PREFERENCE_LOCALE_NAME = 'LocaleName'; + static const PREFERENCE_THEME_NAME = 'ThemeName'; + + static Future getInstance() async { + _instance ??= Preferences._internal(); + _instance1 ??= await SharedPreferences.getInstance(); + return _instance; + } + + String get localeName => _instance1.getString(PREFERENCE_LOCALE_NAME); + + set localeName(String localeName) { + _instance1.setString(PREFERENCE_LOCALE_NAME, localeName); + } + + String get themeName => _instance1.getString(PREFERENCE_THEME_NAME); + + set themeName(String themeName) { + _instance1.setString(PREFERENCE_THEME_NAME, themeName); + } +} diff --git a/lib/routes.dart b/lib/routes.dart new file mode 100644 index 00000000..2cfe5891 --- /dev/null +++ b/lib/routes.dart @@ -0,0 +1,10 @@ +class Routes { + static final splash = '/'; + static final home = '/home'; + static final gameConfig = '/gameConfig'; + static final game = '/game'; + static final account = '/account'; + static final language = '/language'; + static final theme = '/theme'; + static final instrument = '/instrument'; +} diff --git a/lib/search/search_widget.dart b/lib/search/search_widget.dart new file mode 100644 index 00000000..2a96fa9d --- /dev/null +++ b/lib/search/search_widget.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../loading_widget.dart'; +import '../routes.dart'; +import '../songs/song.dart'; +import '../songs/song_widget.dart'; +import '../songs/songs_repository.dart'; + +class SearchWidget extends SearchDelegate { + @override + List buildActions(BuildContext context) { + return [ + IconButton( + icon: Icon(Icons.close), + onPressed: () { + query = ''; + }, + ), + ]; + } + + @override + ThemeData appBarTheme(BuildContext context) { + final theme = Theme.of(context); + if (theme.brightness == Brightness.light) return super.appBarTheme(context); + return theme; + } + + @override + Widget buildLeading(BuildContext context) { + return BackButton( + onPressed: () { + Navigator.pop(context); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + if (query.isEmpty) { + return Container(); + } else { + final songsRepository = RepositoryProvider.of(context); + return FutureBuilder>( + future: songsRepository.searchSongs(query), + builder: (context, AsyncSnapshot> recentList) { + if (recentList.connectionState == ConnectionState.done) { + final songs = recentList.data; + return ListView.builder( + itemCount: songs.length, + itemBuilder: (context, index) { + final song = songs[index]; + return SongWidget( + song: song, + onTap: () async { + await Navigator.pushNamed(context, Routes.gameConfig, + arguments: song); + }, + ); + }, + ); + } else { + return LoadingWidget(); + } + }); + } + } + + @override + Widget buildSuggestions(BuildContext context) { + return Container( + child: LoadingWidget(), + ); + } +} diff --git a/lib/setting/locale_widget.dart b/lib/setting/locale_widget.dart new file mode 100644 index 00000000..3aae6534 --- /dev/null +++ b/lib/setting/locale_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../generated/l10n.dart'; +import 'setting_bloc.dart'; + +const map = {'en': 'English', 'ko': '한국어', 'vi': 'Tiếng Việt', 'zh': '汉语'}; + +class LocaleWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).txt_select_language, + style: Theme.of(context).appBarTheme.textTheme.headline5)), + body: Scrollbar( + isAlwaysShown: true, + controller: scrollController, + child: ListView.builder( + controller: scrollController, + itemCount: S.delegate.supportedLocales.length, + itemBuilder: (context, index) { + return RadioListTile( + title: Text( + map[S.delegate.supportedLocales[index].languageCode], + style: Theme.of(context).textTheme.headline6), + value: S.delegate.supportedLocales[index], + groupValue: Localizations.localeOf(context), + onChanged: (Locale value) { + BlocProvider.of(context) + ..add( + ChangeLocaleEvent(S.delegate.supportedLocales[index])); + }, + ); + }, + ), + )); + } +} diff --git a/lib/setting/setting_bloc.dart b/lib/setting/setting_bloc.dart new file mode 100644 index 00000000..9a60dbeb --- /dev/null +++ b/lib/setting/setting_bloc.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +import '../preferences.dart'; + +part 'setting_event.dart'; +part 'setting_state.dart'; + +class SettingBloc extends Bloc { + SettingBloc() : super(SettingState(Locale('en', ''), ThemeMode.system)) { + Preferences.getInstance().then((preferences) { + if (preferences.localeName != null) { + add(ChangeLocaleEvent( + Locale.fromSubtags(languageCode: preferences.localeName))); + } + if (preferences.themeName != null) { + add(ChangeThemeEvent(ThemeMode.values + .firstWhere((e) => e.toString() == preferences.themeName))); + } + }); + } + + @override + Stream mapEventToState( + SettingEvent event, + ) async* { + if (event is ChangeLocaleEvent) { + await Preferences.getInstance().then((preferences) { + preferences.localeName = event.locale.languageCode; + }); + yield SettingState(event.locale, state.themeMode); + } else if (event is ChangeThemeEvent) { + await Preferences.getInstance().then((preferences) { + preferences.themeName = event.theme.toString(); + }); + yield SettingState(state.locale, event.theme); + } + } +} diff --git a/lib/setting/setting_event.dart b/lib/setting/setting_event.dart new file mode 100644 index 00000000..410a725b --- /dev/null +++ b/lib/setting/setting_event.dart @@ -0,0 +1,23 @@ +part of 'setting_bloc.dart'; + +abstract class SettingEvent extends Equatable { + const SettingEvent(); +} + +class ChangeLocaleEvent extends SettingEvent { + final Locale locale; + + ChangeLocaleEvent(this.locale); + + @override + List get props => [locale]; +} + +class ChangeThemeEvent extends SettingEvent { + final ThemeMode theme; + + ChangeThemeEvent(this.theme); + + @override + List get props => [theme]; +} diff --git a/lib/setting/setting_state.dart b/lib/setting/setting_state.dart new file mode 100644 index 00000000..5a1db86e --- /dev/null +++ b/lib/setting/setting_state.dart @@ -0,0 +1,11 @@ +part of 'setting_bloc.dart'; + +class SettingState extends Equatable { + final Locale locale; + final ThemeMode themeMode; + + SettingState(this.locale, this.themeMode); + + @override + List get props => [locale, themeMode]; +} diff --git a/lib/setting/theme_widget.dart b/lib/setting/theme_widget.dart new file mode 100644 index 00000000..19992a9d --- /dev/null +++ b/lib/setting/theme_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; + +import '../generated/l10n.dart'; +import 'setting_bloc.dart'; + +class ThemeWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder(builder: (context, state) { + final scrollController = ScrollController(); + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).txt_theme, + style: Theme.of(context).appBarTheme.textTheme.headline5)), + body: Scrollbar( + isAlwaysShown: true, + controller: scrollController, + child: ListView.builder( + controller: scrollController, + itemCount: ThemeMode.values.length, + itemBuilder: (context, index) { + return RadioListTile( + title: Text( + Intl.message( + '', + name: + ThemeMode.values[index].toString().split('.').last, + desc: '', + args: [], + ), + style: Theme.of(context).textTheme.headline6), + value: ThemeMode.values[index], + groupValue: state.themeMode, + onChanged: (ThemeMode value) { + BlocProvider.of(context) + ..add(ChangeThemeEvent(value)); + }, + ); + }, + ), + )); + }); + } +} diff --git a/lib/simple_bloc_observer.dart b/lib/simple_bloc_observer.dart new file mode 100644 index 00000000..3de415c2 --- /dev/null +++ b/lib/simple_bloc_observer.dart @@ -0,0 +1,21 @@ +import 'package:bloc/bloc.dart'; + +class SimpleBlocObserver extends BlocObserver { + @override + void onEvent(Bloc bloc, Object event) { + print(event); + super.onEvent(bloc, event); + } + + @override + void onTransition(Bloc bloc, Transition transition) { + print(transition); + super.onTransition(bloc, transition); + } + + @override + void onError(Cubit cubit, Object error, StackTrace stackTrace) { + print(error); + super.onError(cubit, error, stackTrace); + } +} diff --git a/lib/song_checker.dart b/lib/song_checker.dart new file mode 100644 index 00000000..a6f38ff9 --- /dev/null +++ b/lib/song_checker.dart @@ -0,0 +1,119 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:dart_midi/dart_midi.dart'; + +import 'game/tile/tile_chunk.dart'; +import 'game/tile/tile_converter.dart'; + +void main() { + File('database/db.json') + .readAsString() + .then((fileContents) => json.decode(fileContents)) + .then((jsonData) { + final dir = Directory('storage/songs'); + final collections = jsonData['__collections__']; + final songsMap = {}; + dir.listSync(recursive: true).whereType().toList().forEach((file) { + final key = file.uri.toString().split('/').last.replaceAll('.mid', ''); + final temp = file.uri.toString().split('/'); + final genre = temp[temp.length - 2]; + print( + '*************************** Checking $key ***************************'); + final artist = key.split('-').first; + final title = key.replaceAll('$artist-', '').replaceAll('_', ' '); + final artist1 = artist.replaceAll('_', ' '); + final midiFile = MidiParser().parseMidiFromFile(File(file.path)); + final events = midiFile.tracks[0]; + final track = midiFile.tracks.length; + final value = {}; + if (track != 2) { + print('WARNING trackCount $track'); + } + for (final midiEvent in events) { + if (midiEvent is SetTempoEvent) { + final tempo = 60000000 ~/ midiEvent.microsecondsPerBeat; + print('Tempo is $tempo'); + value['bpm'] = tempo; + break; + } + } + final tileChunks = createTileChunks(midiFile); + final groupByDurationToPrevious = Map.fromEntries(groupBy( + tileChunks, (TileChunk tileChunk) => tileChunk.durationToPrevious) + .entries + .toList() + ..sort((e1, e2) => e1.key.compareTo(e2.key))); + final countDurationToPrevious = { + for (var e in groupByDurationToPrevious.keys) + e: groupByDurationToPrevious[e].length + }; + + final sortCountDurationToPrevious = Map.fromEntries( + countDurationToPrevious.entries.toList() + ..sort((e1, e2) => e1.value.compareTo(e2.value))); + final unitDuration = sortCountDurationToPrevious.keys.last; + + groupByDurationToPrevious.entries.forEach((it) { + print('There are ${it.value.length} tile with duration ${it.key}'); + }); + + final tiles = createTiles(tileChunks, unitDuration, NUMBER_TILE_COLUMN); + print('There are ${tiles.length} tiles'); + var tick2Second = + tickToSecond(midiFile.header.ticksPerBeat, value['bpm'] as int); + var speedDpsPerTick = UNIT_DURATION_HEIGHT / unitDuration; + var speedDpsPerSecond = speedDpsPerTick / tick2Second; + final singleTileSeconds = unitDuration * tick2Second; + if (singleTileSeconds < 0.2) { + final newBpm = + ((value['bpm'] as int) * (singleTileSeconds / 0.2)).toInt(); + print('WARNING Too fast $singleTileSeconds $key, set bpm to $newBpm'); + value['bpm'] = newBpm; + tick2Second = + tickToSecond(midiFile.header.ticksPerBeat, value['bpm'] as int); + speedDpsPerTick = UNIT_DURATION_HEIGHT / unitDuration; + speedDpsPerSecond = speedDpsPerTick / tick2Second; + } + final tileCount = tiles.length; + if (tileCount < 50) { + print('WARNING Number tile to small $key, number tiles $tileCount'); + } + final duration = + (0.5 + ((0.0 - tiles.last.initialY) * 1000000) / speedDpsPerSecond) + .toInt(); + print('Duration is $duration microseconds'); + if (duration > 400000000) { + print('WARNING Too long ${duration / 60000000} minutes'); + } + value['tilesCount'] = [ + createTiles(tileChunks, unitDuration, 2).length, + createTiles(tileChunks, unitDuration, 3).length, + createTiles(tileChunks, unitDuration, 4).length + ]; + value['duration'] = [ + (0.5 + + ((0.0 - tiles.last.initialY) * 1000000) / + (speedDpsPerSecond * 0.75)) + .toInt(), + (0.5 + ((0.0 - tiles.last.initialY) * 1000000) / speedDpsPerSecond) + .toInt(), + (0.5 + + ((0.0 - tiles.last.initialY) * 1000000) / + (speedDpsPerSecond * 1.25)) + .toInt() + ]; + value['artist'] = artist1; + value['id'] = key; + value['tags'] = [genre]; + value['title'] = title; + value['url'] = file.uri.toString().replaceAll('storage/', ''); + value['__collections__'] = {}; + songsMap[key] = value; + }); + collections['songs'] = songsMap; + jsonData['__collections__'] = collections; + File('database/db.json').writeAsStringSync(json.encode(jsonData)); + }); +} diff --git a/lib/songs/song.dart b/lib/songs/song.dart new file mode 100644 index 00000000..e0ef8f96 --- /dev/null +++ b/lib/songs/song.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'song.g.dart'; + +@JsonSerializable(explicitToJson: true) +class Song { + String id; + String title; + String artist; + String url; + int bpm; + List tilesCount; + List duration; + List tags; + + Song( + this.id, + this.title, + this.artist, + this.url, + this.bpm, + this.tilesCount, + this.duration, + this.tags, + ); + + factory Song.fromJson(Map json) => _$SongFromJson(json); + + Map toJson() => _$SongToJson(this); +} diff --git a/lib/songs/song.g.dart b/lib/songs/song.g.dart new file mode 100644 index 00000000..b248ffd2 --- /dev/null +++ b/lib/songs/song.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'song.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Song _$SongFromJson(Map json) { + return Song( + json['id'] as String, + json['title'] as String, + json['artist'] as String, + json['url'] as String, + json['bpm'] as int, + (json['tilesCount'] as List)?.map((e) => e as int)?.toList(), + (json['duration'] as List)?.map((e) => e as int)?.toList(), + (json['tags'] as List)?.map((e) => e as String)?.toList(), + ); +} + +Map _$SongToJson(Song instance) => { + 'id': instance.id, + 'title': instance.title, + 'artist': instance.artist, + 'url': instance.url, + 'bpm': instance.bpm, + 'tilesCount': instance.tilesCount, + 'duration': instance.duration, + 'tags': instance.tags, + }; diff --git a/lib/songs/song_widget.dart b/lib/songs/song_widget.dart new file mode 100644 index 00000000..a1a7b189 --- /dev/null +++ b/lib/songs/song_widget.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'song.dart'; + +class SongWidget extends StatelessWidget { + final GestureTapCallback onTap; + final Song song; + + SongWidget({ + Key key, + @required this.onTap, + @required this.song, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: onTap, + title: Text( + song.title, + style: Theme.of(context).textTheme.headline6, + ), + subtitle: song.artist.isNotEmpty + ? Text( + song.artist, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.subtitle1, + ) + : null, + ); + } +} diff --git a/lib/songs/songs_bloc.dart b/lib/songs/songs_bloc.dart new file mode 100644 index 00000000..316c79e7 --- /dev/null +++ b/lib/songs/songs_bloc.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +import '../main.dart'; +import 'song.dart'; +import 'songs_event.dart'; +import 'songs_repository.dart'; +import 'songs_state.dart'; + +class SongsBloc extends Bloc { + final SongsRepository _songsRepository; + + SongsBloc({@required SongsRepository songsRepository}) + : assert(songsRepository != null), + _songsRepository = songsRepository, + super(SongsInitial()); + + @override + Stream mapEventToState(SongsEvent event) async* { + if (event is LoadMoreSongsByTagNumbers) { + yield* _mapLoadMoreSongsToState(event); + } + if (event is UpdateSongs) { + yield SongsLoaded( + event.songsByTags, event.isLoadingMoreByTags, event.isLoadedByTags); + } + } + + Stream _mapLoadMoreSongsToState( + LoadMoreSongsByTagNumbers event) async* { + final songsByTags = (state is SongsLoaded) + ? (state as SongsLoaded).songsByTags + : songTags.map((e) => []).toList(); + final isLoadingMoreByTags = (state is SongsLoaded) + ? (state as SongsLoaded).isLoadingMoreByTags + : songTags.map((e) => false).toList(); + final isLoadedByTags = (state is SongsLoaded) + ? (state as SongsLoaded).isLoadedByTags + : songTags.map((e) => false).toList(); + + event.tagNumbers.forEach((tag) async { + if (!isLoadingMoreByTags[tag] && !isLoadedByTags[tag]) { + isLoadingMoreByTags[tag] = true; + add(UpdateSongs(songsByTags, isLoadingMoreByTags, isLoadedByTags)); + final songs = await _songsRepository.songsByTag(songTags[tag], + songsByTags[tag].isEmpty ? '' : songsByTags[tag].last.title, 20); + + songsByTags[tag] += songs; + isLoadingMoreByTags[tag] = false; + isLoadedByTags[tag] = songs.isEmpty; + add(UpdateSongs(songsByTags, isLoadingMoreByTags, isLoadedByTags)); + } + }); + } +} diff --git a/lib/songs/songs_event.dart b/lib/songs/songs_event.dart new file mode 100644 index 00000000..d4758777 --- /dev/null +++ b/lib/songs/songs_event.dart @@ -0,0 +1,28 @@ +import 'package:equatable/equatable.dart'; + +import 'song.dart'; + +abstract class SongsEvent extends Equatable { + const SongsEvent(); + + @override + List get props => []; +} + +class LoadMoreSongsByTagNumbers extends SongsEvent { + final List tagNumbers; + + LoadMoreSongsByTagNumbers(this.tagNumbers); +} + +class UpdateSongs extends SongsEvent { + final List> songsByTags; + final List isLoadingMoreByTags; + final List isLoadedByTags; + + const UpdateSongs( + this.songsByTags, this.isLoadingMoreByTags, this.isLoadedByTags); + + @override + List get props => [songsByTags, isLoadingMoreByTags, isLoadedByTags]; +} diff --git a/lib/songs/songs_repository.dart b/lib/songs/songs_repository.dart new file mode 100644 index 00000000..6e6a43ca --- /dev/null +++ b/lib/songs/songs_repository.dart @@ -0,0 +1,9 @@ +import 'dart:async'; + +import 'song.dart'; + +abstract class SongsRepository { + Future> songsByTag(String tag, String titleStart, int limit); + + Future> searchSongs(String text); +} diff --git a/lib/songs/songs_repository_impl.dart b/lib/songs/songs_repository_impl.dart new file mode 100644 index 00000000..30ba4dc4 --- /dev/null +++ b/lib/songs/songs_repository_impl.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; + +import 'song.dart'; +import 'songs_repository.dart'; + +class SongsRepositoryImpl implements SongsRepository { + @override + Future> songsByTag( + String tag, String titleStart, int limit) async { + return (await FirebaseFirestore.instance + .collection('songs') + .orderBy('title') + .where('tags', arrayContains: tag) + .startAfter([titleStart]) + .limit(limit) + .get()) + .docs + .map((e) => Song.fromJson(e.data())) + .toList(); + } + + @override + Future> searchSongs(String text) async { + return (await FirebaseFirestore.instance + .collection('songs') + .where('title', isGreaterThanOrEqualTo: text) + .where('title', isLessThanOrEqualTo: text + '\uf8ff') + .get()) + .docs + .map((e) => Song.fromJson(e.data())) + .toList(); + } +} diff --git a/lib/songs/songs_state.dart b/lib/songs/songs_state.dart new file mode 100644 index 00000000..a0f52420 --- /dev/null +++ b/lib/songs/songs_state.dart @@ -0,0 +1,18 @@ +import 'song.dart'; + +abstract class SongsState { + const SongsState(); +} + +class SongsInitial extends SongsState { + const SongsInitial(); +} + +class SongsLoaded extends SongsState { + final List> songsByTags; + final List isLoadingMoreByTags; + final List isLoadedByTags; + + const SongsLoaded( + this.songsByTags, this.isLoadingMoreByTags, this.isLoadedByTags); +} diff --git a/lib/songs/songs_widget.dart b/lib/songs/songs_widget.dart new file mode 100644 index 00000000..0738529b --- /dev/null +++ b/lib/songs/songs_widget.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../loading_widget.dart'; +import '../routes.dart'; +import 'song_widget.dart'; +import 'songs_bloc.dart'; +import 'songs_event.dart'; +import 'songs_state.dart'; + +class SongsWidget extends StatelessWidget { + final int tagNumber; + + SongsWidget({Key key, @required int tagNumber}) + : tagNumber = tagNumber, + super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is SongsInitial) { + return LoadingWidget(); + } else if (state is SongsLoaded) { + final songsByTag = state.songsByTags[tagNumber]; + return Scrollbar( + child: NotificationListener( + child: ListView.separated( + itemCount: (state.isLoadingMoreByTags[tagNumber]) + ? songsByTag.length + 1 + : songsByTag.length, + itemBuilder: (context, index) { + if (index == songsByTag.length) { + return Center( + child: SizedBox( + child: Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), + height: 32, + width: 32, + ), + ); + } + final song = songsByTag[index]; + return SongWidget( + song: song, + onTap: () async { + await Navigator.pushNamed(context, Routes.gameConfig, + arguments: song); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + height: 4, + ); + }, + ), + onNotification: (notification) { + if (notification.metrics.pixels > 0 && + notification.metrics.atEdge) { + BlocProvider.of(context) + .add(LoadMoreSongsByTagNumbers([tagNumber])); + } + return true; + }, + ), + ); + } else { + return Container(); + } + }, + ); + } +} diff --git a/lib/splash_widget.dart b/lib/splash_widget.dart new file mode 100644 index 00000000..7a7974bf --- /dev/null +++ b/lib/splash_widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'authentication/authentication_bloc.dart'; +import 'authentication/authentication_event.dart'; +import 'authentication/authentication_state.dart'; +import 'routes.dart'; + +class SplashWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + BlocProvider.of(context)..add(SignInAnonymouslyEvent()); + BlocProvider.of(context).listen((state) { + if (state is Authenticated) { + Navigator.pushNamedAndRemoveUntil( + context, Routes.home, (route) => false); + } + }); + return Scaffold( + body: Center( + child: Image(image: AssetImage('assets/images/img_app_icon.png')))); + } +} diff --git a/lib/user/user.dart b/lib/user/user.dart new file mode 100644 index 00000000..f0b470ab --- /dev/null +++ b/lib/user/user.dart @@ -0,0 +1,28 @@ +import 'dart:core'; + +import 'package:json_annotation/json_annotation.dart'; + +part 'user.g.dart'; + +@JsonSerializable(explicitToJson: true) +class User { + String id; + int playedNotes; + int stars; + Duration playedTime; + String instrumentId; + List notificationTokens; + + User( + this.id, + this.playedNotes, + this.playedTime, + this.stars, + this.instrumentId, + this.notificationTokens, + ); + + factory User.fromJson(Map json) => _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} diff --git a/lib/user/user.g.dart b/lib/user/user.g.dart new file mode 100644 index 00000000..04be7b02 --- /dev/null +++ b/lib/user/user.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) { + return User( + json['id'] as String, + json['playedNotes'] as int, + json['playedTime'] == null + ? null + : Duration(microseconds: json['playedTime'] as int), + json['stars'] as int, + json['instrumentId'] as String, + (json['notificationTokens'] as List)?.map((e) => e as String)?.toList(), + ); +} + +Map _$UserToJson(User instance) => { + 'id': instance.id, + 'playedNotes': instance.playedNotes, + 'stars': instance.stars, + 'playedTime': instance.playedTime?.inMicroseconds, + 'instrumentId': instance.instrumentId, + 'notificationTokens': instance.notificationTokens, + }; diff --git a/lib/user/user_bloc.dart b/lib/user/user_bloc.dart new file mode 100644 index 00000000..cab58352 --- /dev/null +++ b/lib/user/user_bloc.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +import '../instrument/instrument.dart'; +import '../instrument/instruments_repository.dart'; +import 'user_repository.dart'; + +part 'user_event.dart'; +part 'user_state.dart'; + +class UserBloc extends Bloc { + final UserRepository _userRepository; + final InstrumentsRepository _instrumentsRepository; + + UserBloc(this._instrumentsRepository, + {@required UserRepository userRepository}) + : assert(userRepository != null), + _userRepository = userRepository, + super(UserLoading()) { + _userRepository.getCurrentUser().listen((user) async { + final instruments = await _instrumentsRepository.instruments(); + add(UpdateUser(user, instruments)); + }); + } + + @override + Stream mapEventToState( + UserEvent event, + ) async* { + if (event is UpdateUser) { + yield UserUpdated(event.user, event.instruments); + } else if (event is ChangeInstrument) { + _userRepository.changeInstrument(event.instrumentId); + } + } +} diff --git a/lib/user/user_event.dart b/lib/user/user_event.dart new file mode 100644 index 00000000..222187f6 --- /dev/null +++ b/lib/user/user_event.dart @@ -0,0 +1,24 @@ +part of 'user_bloc.dart'; + +abstract class UserEvent extends Equatable { + const UserEvent(); +} + +class UpdateUser extends UserEvent { + final AppUser user; + final List instruments; + + UpdateUser(this.user, this.instruments); + + @override + List get props => [user, instruments]; +} + +class ChangeInstrument extends UserEvent { + final String instrumentId; + + ChangeInstrument(this.instrumentId); + + @override + List get props => [instrumentId]; +} diff --git a/lib/user/user_repository.dart b/lib/user/user_repository.dart new file mode 100644 index 00000000..5422d33c --- /dev/null +++ b/lib/user/user_repository.dart @@ -0,0 +1,20 @@ +import 'user.dart'; + +abstract class UserRepository { + void changUser(); + + void changeInstrument(String instrumentId); + + Stream getCurrentUser(); +} + +class AppUser { + final String name; + final String photoUrl; + final bool isAnonymous; + final DateTime creationTime; + final User user; + + AppUser( + this.name, this.photoUrl, this.user, this.isAnonymous, this.creationTime); +} diff --git a/lib/user/user_repository_impl.dart b/lib/user/user_repository_impl.dart new file mode 100644 index 00000000..070ef408 --- /dev/null +++ b/lib/user/user_repository_impl.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../util.dart'; +import 'user.dart' as user; +import 'user_repository.dart'; + +class UserRepositoryImpl implements UserRepository { + UserRepositoryImpl(); + + StreamSubscription _userSubscription; + final _userController = BehaviorSubject(); + + @override + Stream getCurrentUser() { + return _userController.stream; + } + + @override + Future changUser() async { + await _userSubscription?.cancel(); + _userSubscription = FirebaseFirestore.instance + .collection('users') + .doc(FirebaseAuth.instance.currentUser.uid) + .snapshots() + .map((event) { + var photoUrl = ''; + var name = ''; + FirebaseAuth.instance.currentUser.providerData.forEach((userInfo) { + if (userInfo.providerId == 'facebook.com') { + photoUrl = '${userInfo.photoURL}?height=${96.toPixel()}'; + } else if (userInfo.providerId == 'google.com') { + photoUrl = + userInfo.photoURL.replaceAll('s96-c', 's${96.toPixel()}-c'); + } + name = userInfo.displayName; + }); + return AppUser( + name, + photoUrl, + user.User.fromJson(event.data()), + FirebaseAuth.instance.currentUser.isAnonymous, + FirebaseAuth.instance.currentUser.metadata.creationTime); + }).listen((user) { + _userController.add(user); + }); + } + + @override + void changeInstrument(String instrumentId) { + FirebaseFirestore.instance + .collection('users') + .doc(FirebaseAuth.instance.currentUser.uid) + .update({'instrumentId': instrumentId}); + } +} diff --git a/lib/user/user_state.dart b/lib/user/user_state.dart new file mode 100644 index 00000000..d84ab1d0 --- /dev/null +++ b/lib/user/user_state.dart @@ -0,0 +1,20 @@ +part of 'user_bloc.dart'; + +abstract class UserState extends Equatable { + const UserState(); +} + +class UserLoading extends UserState { + @override + List get props => []; +} + +class UserUpdated extends UserState { + final AppUser user; + final List instruments; + + UserUpdated(this.user, this.instruments); + + @override + List get props => [user, instruments]; +} diff --git a/lib/user/user_widget.dart b/lib/user/user_widget.dart new file mode 100644 index 00000000..718228b3 --- /dev/null +++ b/lib/user/user_widget.dart @@ -0,0 +1,275 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; + +import '../authentication/authentication_bloc.dart'; +import '../authentication/authentication_event.dart'; +import '../generated/l10n.dart'; +import '../loading_widget.dart'; +import '../util.dart'; +import 'user_bloc.dart'; + +class UserWidget extends StatelessWidget { + UserWidget({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).txt_page_title_account, + style: Theme.of(context).appBarTheme.textTheme.headline5)), + body: (() { + if (state is UserLoading) { + return LoadingWidget(); + } else if (state is UserUpdated) { + final scrollController = ScrollController(); + return Padding( + padding: const EdgeInsets.all(8), + child: Scrollbar( + isAlwaysShown: true, + controller: scrollController, + child: ListView( + shrinkWrap: true, + controller: scrollController, + children: [buildColumn(state, context)]))); + } else { + return Container(); + } + }())); + }, + ); + } + + Column buildColumn(UserUpdated state, BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + () { + if (!state.user.isAnonymous) { + return Card( + color: Colors.transparent, + elevation: 0, + child: Container( + padding: EdgeInsets.all(8.0), + child: Row( + children: [ + ClipOval( + child: CachedNetworkImage( + width: 72, + height: 72, + imageUrl: state.user.photoUrl, + placeholder: (context, url) => + CircularProgressIndicator(), + memCacheWidth: 72.toPixel(), + memCacheHeight: 72.toPixel()), + ), + SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.user.name, + style: Theme.of(context).textTheme.headline5, + ), + SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.today, + size: 16, + ), + SizedBox(width: 8), + Text( + S.of(context).txt_joined(DateFormat.yMMMd() + .format(state.user.creationTime)), + style: Theme.of(context).textTheme.subtitle1, + ), + ], + ), + Row( + children: [ + Image( + image: AssetImage( + 'assets/images/img_guitar.png'), + width: 16, + height: 16, + ), + SizedBox(width: 8), + Text(S.of(context).txt_using(Intl.message( + '', + /* FIXME Localization name of instrument should be taken from server, not from local text resources */ + name: state.user.user.instrumentId, + desc: '', + args: [], + ))), + ], + ), + ], + ) + ], + ))); + } else { + return Card( + color: Colors.transparent, + elevation: 0, + child: Container( + padding: EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OutlinedButton( + onPressed: () { + BlocProvider.of(context) + ..add(SignInWithGoogleEvent()); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image( + image: AssetImage( + 'assets/images/img_google.png')), + SizedBox(width: 8), + Text(S.of(context).txt_button_sign_in_google, + style: + Theme.of(context).textTheme.subtitle1) + ], + ), + ), + ), + SizedBox(height: 8), + OutlinedButton( + onPressed: () { + BlocProvider.of(context) + ..add(SignInWithFacebookEvent()); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image( + image: AssetImage( + 'assets/images/img_facebook.png')), + SizedBox(width: 8), + Text( + S.of(context).txt_button_sign_in_facebook, + style: + Theme.of(context).textTheme.subtitle1) + ], + ), + ), + ), + ]))); + } + }(), + Card( + color: Colors.transparent, + elevation: 0, + child: Table( + children: [ + TableRow(children: [ + Card( + color: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + // if you need this + side: BorderSide( + color: Colors.grey.withOpacity(0.2), + width: 1, + ), + ), + child: Container( + //color: Colors.white, + //width: 200, + //height: 200, + child: Column( + children: [ + SizedBox(height: 8), + Image( + image: AssetImage('assets/images/img_star.png'), + ), + SizedBox(height: 8), + Text(state.user.user.stars.toString(), + style: Theme.of(context).textTheme.subtitle1), + SizedBox(height: 8), + ], + ), + ), + ), + Card( + color: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + // if you need this + side: BorderSide( + color: Colors.grey.withOpacity(0.2), + width: 1, + ), + ), + child: Container( + //color: Colors.white, + // width: 200, + //height: 200, + child: Column( + children: [ + SizedBox(height: 8), + Image( + image: AssetImage('assets/images/img_note.png'), + ), + SizedBox(height: 8), + Text(state.user.user.playedNotes.toString(), + style: Theme.of(context).textTheme.subtitle1), + SizedBox(height: 8), + ], + ), + ), + ), + Card( + color: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + // if you need this + side: BorderSide( + color: Colors.grey.withOpacity(0.2), + width: 1, + ), + ), + child: Container( + //color: Colors.white, + //width: 200, + //height: 200, + child: Column( + children: [ + SizedBox(height: 8), + Image( + image: AssetImage('assets/images/img_clock.png'), + ), + SizedBox(height: 8), + Text( + state.user.user.playedTime + .toString() + .substring(0, 4) + .toString(), + style: Theme.of(context).textTheme.subtitle1), + SizedBox(height: 8), + ], + ), + ), + ), + ]), + ], + )), + ], + ); + } +} diff --git a/lib/users_mitigator.dart b/lib/users_mitigator.dart new file mode 100644 index 00000000..f9e926cd --- /dev/null +++ b/lib/users_mitigator.dart @@ -0,0 +1,29 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +void main() { + File('database/db.json') + .readAsString() + .then((fileContents) => json.decode(fileContents)) + .then((jsonData) { + final collections = jsonData['__collections__']; + final users = collections['users']; + (users as Map).forEach((key, value) { + final newValue = HashMap(); + newValue['__collections__'] = value['__collections__']; + newValue['id'] = value['id']; + newValue['playedNotes'] = + (value['playedNotes'] == null) ? 0 : value['playedNotes']; + newValue['stars'] = (value['stars'] == null) ? 0 : value['stars']; + newValue['playedTime'] = + (value['playedTime'] == null) ? 0 : value['playedTime']; + newValue['instrumentId'] = value['instrumentId']; + newValue['notificationTokens'] = value['notificationTokens']; + users[key] = newValue; + }); + collections['users'] = users; + jsonData['__collections__'] = collections; + File('database/db.json').writeAsStringSync(json.encode(jsonData)); + }); +} diff --git a/lib/util.dart b/lib/util.dart new file mode 100644 index 00000000..12d45aad --- /dev/null +++ b/lib/util.dart @@ -0,0 +1,46 @@ +import 'dart:collection'; +import 'dart:ui'; + +import 'game/tile/tile_converter.dart'; + +final screenWidth = window.physicalSize.width / window.devicePixelRatio; +final screenHeight = window.physicalSize.height / window.devicePixelRatio; +String nearestDevicePixelRatioFolder = () { + final candidates = SplayTreeMap.from( + {1: '', 1.5: '1.5x/', 2.0: '2.0x/', 3.0: '3.0x/', 4.0: '4.0x/'}); + final value = window.devicePixelRatio; + if (candidates.containsKey(value)) { + return candidates[value]; + } + final lower = candidates.lastKeyBefore(value); + final upper = candidates.firstKeyAfter(value); + if (lower == null) { + return candidates[upper]; + } + if (upper == null) { + return candidates[lower]; + } + if (value > (lower + upper) / 2) { + return candidates[upper]; + } else { + return candidates[lower]; + } +}(); + +extension DpToPixelConverter on int { + int toPixel() { + return (this * window.devicePixelRatio).toInt(); + } +} + +final positionsX = () { + final positionXs = []; + const TILE_PAD = 24; + final paddedTileWidth = TILE_WIDTH + 2 * TILE_PAD; + final padLeftRight = (screenWidth - NUMBER_TILE_COLUMN * paddedTileWidth) / 2; + for (var column = 0; column < NUMBER_TILE_COLUMN; column++) { + positionXs.add(padLeftRight + column * paddedTileWidth + TILE_PAD); + } + return positionXs; +}(); +final pauseY = screenHeight - 48; diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..31b944b0 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1045 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "14.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.41.1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.2" + audioplayers: + dependency: transitive + description: + name: audioplayers + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.1" + bloc: + dependency: transitive + description: + name: bloc + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.1" + box2d_flame: + dependency: transitive + description: + name: box2d_flame + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.6" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.5" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.12" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.6" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "7.1.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.3" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.4" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1+2" + cloud_functions: + dependency: "direct main" + description: + name: cloud_functions + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.2" + cloud_functions_platform_interface: + dependency: transitive + description: + name: cloud_functions_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.3" + cloud_functions_web: + dependency: transitive + description: + name: cloud_functions_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0-nullsafety.3" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + dart_midi: + dependency: "direct main" + description: + name: dart_midi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.10" + device_info: + dependency: "direct main" + description: + name: device_info + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + device_info_platform_interface: + dependency: transitive + description: + name: device_info_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.5" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + url: "https://pub.dartlang.org" + source: hosted + version: "0.18.4+1" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.2+3" + firebase_core: + dependency: transitive + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.3" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1+1" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + firebase_dynamic_links: + dependency: "direct main" + description: + name: firebase_dynamic_links + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + firebase_performance: + dependency: "direct main" + description: + name: firebase_performance + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" + flame: + dependency: "direct main" + description: + name: flame + url: "https://pub.dartlang.org" + source: hosted + version: "0.28.0" + flare_dart: + dependency: transitive + description: + name: flare_dart + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.4" + flare_flutter: + dependency: transitive + description: + name: flare_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.1" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + flutter_cache_manager_firebase: + dependency: "direct main" + description: + name: flutter_cache_manager_firebase + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_email_sender: + dependency: "direct main" + description: + name: flutter_email_sender + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + flutter_facebook_login: + dependency: "direct main" + description: + name: flutter_facebook_login + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.9" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.19" + in_app_review: + dependency: "direct main" + description: + name: in_app_review + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + in_app_review_platform_interface: + dependency: transitive + description: + name: in_app_review_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.4" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.9" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0-nullsafety.3" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + octo_image: + dependency: transitive + description: + name: octo_image + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + ordered_set: + dependency: transitive + description: + name: ordered_set + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + package_info: + dependency: "direct main" + description: + name: package_info + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3+2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0-nullsafety.1" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.27" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+8" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+3" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.2" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.9" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.13" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2+3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.7" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + rxdart: + dependency: "direct main" + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.25.0" + share: + dependency: "direct main" + description: + name: share + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.5+4" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.12+4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+4" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+11" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+7" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+3" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.9" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + soundpool: + dependency: "direct main" + description: + path: soundpool + ref: HEAD + resolved-ref: "46750cf7507a9aba1779032e49d2b761eec763ca" + url: "https://github.com/cuong0993/soundpool.git" + source: git + version: "1.1.2" + soundpool_platform_interface: + dependency: "direct main" + description: + path: soundpool_platform_interface + ref: HEAD + resolved-ref: "46750cf7507a9aba1779032e49d2b761eec763ca" + url: "https://github.com/cuong0993/soundpool.git" + source: git + version: "1.0.1" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.10+1" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + sqflite: + dependency: transitive + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2+1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.6" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0+2" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0-nullsafety.3" + url_launcher: + dependency: transitive + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "5.7.10" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+4" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+9" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.9" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5+1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+3" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.2" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.3" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+15" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.1" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" +sdks: + dart: ">=2.10.2 <2.11.0" + flutter: ">=1.22.2 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..fa055bda --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,61 @@ +name: hitnotes +description: An application. +version: 1.0.0+1 + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + flutter_bloc: ^6.0.6 + equatable: ^1.2.5 + cloud_firestore: ^0.14.1+3 + firebase_auth: ^0.18.3 + firebase_storage: ^5.2.0 + cloud_functions: ^0.7.0+1 + flame: 0.28.0 + dart_midi: ^1.0.1 + #soundpool: ^1.1.2 + soundpool: + git: + url: https://github.com/cuong0993/soundpool.git + path: soundpool + soundpool_platform_interface: + git: + url: https://github.com/cuong0993/soundpool.git + path: soundpool_platform_interface + percent_indicator: ^2.1.7+4 + cupertino_icons: ^1.0.0 + json_annotation: ^3.1.0 + flutter_cache_manager_firebase: ^1.1.0 + cached_network_image: ^2.5.0 + shared_preferences: ^0.5.12+2 + in_app_review: ^1.0.1+1 + intl: ^0.16.1 + flutter_email_sender: ^4.0.0 + package_info: ^0.4.3 + device_info: ^1.0.0 + share: ^0.6.5+4 + firebase_dynamic_links: ^0.6.1 + google_sign_in: ^4.5.8 + flutter_facebook_login: ^3.0.0 + firebase_performance: ^0.4.1 + rxdart: ^0.25.0 + firebase_crashlytics: ^0.2.3+1 + +dev_dependencies: + pedantic: ^1.9.2 + build_runner: ^1.10.3 + json_serializable: ^3.5.0 + +flutter: + uses-material-design: true + assets: + - assets/images/ +flutter_intl: + enabled: true + localizely: + project_id: 12c28e7e-0600-442d-8f23-3ee5a71d49f4 diff --git a/resources/.gitignore b/resources/.gitignore new file mode 100644 index 00000000..71e41c9c --- /dev/null +++ b/resources/.gitignore @@ -0,0 +1,6 @@ +/.idea/* +!/.idea/codeStyles/ +!/.idea/runConfigurations/ +*.iml +*tmp.png +/__pycache__/* \ No newline at end of file diff --git a/resources/.run/validate_images.run.xml b/resources/.run/validate_images.run.xml new file mode 100644 index 00000000..41d4de0b --- /dev/null +++ b/resources/.run/validate_images.run.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/resources/.run/validate_strings.run.xml b/resources/.run/validate_strings.run.xml new file mode 100644 index 00000000..d759dc86 --- /dev/null +++ b/resources/.run/validate_strings.run.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/resources/android-images/ic_launcher.svg b/resources/android-images/ic_launcher.svg new file mode 100644 index 00000000..66567ffc --- /dev/null +++ b/resources/android-images/ic_launcher.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/android-images/ic_launcher_background.svg b/resources/android-images/ic_launcher_background.svg new file mode 100644 index 00000000..3e3ecee4 --- /dev/null +++ b/resources/android-images/ic_launcher_background.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/android-images/ic_launcher_foreground.svg b/resources/android-images/ic_launcher_foreground.svg new file mode 100644 index 00000000..7b5a3aae --- /dev/null +++ b/resources/android-images/ic_launcher_foreground.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/android-images/ic_launcher_round.svg b/resources/android-images/ic_launcher_round.svg new file mode 100644 index 00000000..9941b09c --- /dev/null +++ b/resources/android-images/ic_launcher_round.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/android-images/img_chaomao.svg b/resources/android-images/img_chaomao.svg new file mode 100644 index 00000000..bf29a8b3 --- /dev/null +++ b/resources/android-images/img_chaomao.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/gen_android-images.py b/resources/gen_android-images.py new file mode 100644 index 00000000..23004581 --- /dev/null +++ b/resources/gen_android-images.py @@ -0,0 +1,45 @@ +import os +import subprocess + +multipliers = [ + 0.75, + 1.0, + 1.5, + 2.0, + 3.0, + 4.0 +] +folders = [ + "mipmap-ldpi", + "mipmap-mdpi", + "mipmap-hdpi", + "mipmap-xhdpi", + "mipmap-xxhdpi", + "mipmap-xxxhdpi" +] +inkscape_default_dpi = 96 + +svg_default_folder = 'android-images' +root = '../android/app/src/main/res' + +file_paths = [] +file_names = os.listdir(svg_default_folder) +for svg_file_name in file_names: + file_paths.append(os.path.join(svg_default_folder, svg_file_name)) + +for index in range(len(multipliers)): + folder = os.path.join(root, folders[index]) + os.makedirs(folder, exist_ok=True) + + dpi = int(inkscape_default_dpi * multipliers[index]) + print('Generating assets for', folder) + for file_path in file_paths: + svg_file_name_with_ext = os.path.basename(file_path) + svg_file_name, svg_file_ext = os.path.splitext(svg_file_name_with_ext) + png_file_path = os.path.join(folder, svg_file_name + '.png') + call_params = ["inkscape", + "--vacuum-defs", + "--export-dpi=%s" % dpi, + "--export-filename=%s" % png_file_path, + file_path] + subprocess.check_call(call_params, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) diff --git a/resources/gen_images.py b/resources/gen_images.py new file mode 100644 index 00000000..be0c0480 --- /dev/null +++ b/resources/gen_images.py @@ -0,0 +1,54 @@ +import os +import shutil +import subprocess +import sys + +# python3 gen_images.py images/file.svg +# if file is not passed, this script will generate all files of default folder + +multipliers = [ + 1.0, + 1.5, + 2.0, + 3.0, + 4.0 +] + +folfers = [ + '', + '1.5x', + '2.0x', + '3.0x', + '4.0x' +] + +inkscape_default_dpi = 96 +root_folder = '../assets/images/' +svg_default_folder = 'images' + +if len(sys.argv) == 1: + file_paths = [] + file_names = os.listdir(svg_default_folder) + for svg_file_name in file_names: + file_paths.append(os.path.join(svg_default_folder, svg_file_name)) + shutil.rmtree(root_folder) +else: + file_paths = [] + for i in range(1, len(sys.argv)): + file_paths.append(sys.argv[i]) + +for index in range(len(multipliers)): + folder = os.path.join(root_folder, str(folfers[index])) + os.makedirs(folder, exist_ok=True) + dpi = int(inkscape_default_dpi * multipliers[index]) + print('Generating assets for', folder) + for file_path in file_paths: + svg_file_name_with_ext = os.path.basename(file_path) + svg_file_name, svg_file_ext = os.path.splitext(svg_file_name_with_ext) + png_file_path = os.path.join(folder, svg_file_name + '.png') + call_params = ["inkscape", + "--vacuum-defs", + "--export-dpi=%s" % dpi, + "--export-filename=%s" % png_file_path, + file_path] + subprocess.check_call(call_params, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) diff --git a/resources/gen_store_images.py b/resources/gen_store_images.py new file mode 100644 index 00000000..b01ab27a --- /dev/null +++ b/resources/gen_store_images.py @@ -0,0 +1,48 @@ +import os +import subprocess +from shutil import copyfile + +inkscape_default_dpi = 96 + +svg_default_folder = 'metadata' +root1 = '../fastlane' + +file_paths = [] +file_paths1 = [] + +for root, subdirs, files in os.walk(svg_default_folder): + for name in files: + if ".svg" in name: + print(os.path.join(root, name)) + file_paths.append(os.path.join(root, name)) + if ".png" in name: + print(os.path.join(root, name)) + if "tmp" in name: + os.remove(os.path.join(root, name)) + print("Removed") + else: + file_paths1.append(os.path.join(root, name)) + +for file_path in file_paths: + png_file_path = os.path.join(root1, file_path.replace("svg", "png")) + os.makedirs(os.path.dirname(png_file_path), exist_ok=True) + call_params = ["inkscape", + "--vacuum-defs", + "--export-dpi=%s" % inkscape_default_dpi, + "--export-filename=%s" % png_file_path, + file_path] + subprocess.check_call(call_params, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + print('Generated {0}'.format(file_path)) + +for file_path in file_paths1: + file_path1 = file_path.replace(".png", "") + png_file_path = os.path.join(root1, file_path) + os.makedirs(os.path.dirname(png_file_path), exist_ok=True) + text_file = open(file_path1 + ".txt") + text = text_file.read() + if 'phoneScreenshots' in file_path: + subprocess.call(['sh', './localize_phone_images.sh', file_path, text]) + elif 'sevenInchScreenshots' in file_path: + subprocess.call(['sh', './localize_tablet_images.sh', file_path, text]) + print('Localized {0}'.format(file_path)) + copyfile(file_path + ".4tmp.png", png_file_path) diff --git a/resources/images/img_app_icon.svg b/resources/images/img_app_icon.svg new file mode 100644 index 00000000..bd40730b --- /dev/null +++ b/resources/images/img_app_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/img_clef.svg b/resources/images/img_clef.svg new file mode 100644 index 00000000..221a31d3 --- /dev/null +++ b/resources/images/img_clef.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_clock.svg b/resources/images/img_clock.svg new file mode 100644 index 00000000..4c578edd --- /dev/null +++ b/resources/images/img_clock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/img_facebook.svg b/resources/images/img_facebook.svg new file mode 100644 index 00000000..1fb9e69a --- /dev/null +++ b/resources/images/img_facebook.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/img_google.svg b/resources/images/img_google.svg new file mode 100644 index 00000000..86d77873 --- /dev/null +++ b/resources/images/img_google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/img_guitar.svg b/resources/images/img_guitar.svg new file mode 100644 index 00000000..00606922 --- /dev/null +++ b/resources/images/img_guitar.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_note.svg b/resources/images/img_note.svg new file mode 100644 index 00000000..229eef33 --- /dev/null +++ b/resources/images/img_note.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_single_note.svg b/resources/images/img_single_note.svg new file mode 100644 index 00000000..d468c5ef --- /dev/null +++ b/resources/images/img_single_note.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_staff.svg b/resources/images/img_staff.svg new file mode 100644 index 00000000..ad901517 --- /dev/null +++ b/resources/images/img_staff.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_star.svg b/resources/images/img_star.svg new file mode 100644 index 00000000..94c3c2b3 --- /dev/null +++ b/resources/images/img_star.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_star_rate.svg b/resources/images/img_star_rate.svg new file mode 100644 index 00000000..39988b10 --- /dev/null +++ b/resources/images/img_star_rate.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_star_rate_disable.svg b/resources/images/img_star_rate_disable.svg new file mode 100644 index 00000000..0c3f3746 --- /dev/null +++ b/resources/images/img_star_rate_disable.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/img_touch.svg b/resources/images/img_touch.svg new file mode 100644 index 00000000..6679af77 --- /dev/null +++ b/resources/images/img_touch.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/localize_phone_images.sh b/resources/localize_phone_images.sh new file mode 100644 index 00000000..98fecda7 --- /dev/null +++ b/resources/localize_phone_images.sh @@ -0,0 +1,7 @@ +# https://imagemagick.org/script/command-line-processing.php +# http://www.imagemagick.org/Usage/thumbnails/#labels +# https://imagemagick.org/Usage/annotating/ +convert $1 \( +clone -alpha extract -draw 'fill black polygon 0,0 0,10 10,0 fill white circle 10,10 10,0' \( +clone -flip \) -compose Multiply -composite \( +clone -flop \) -compose Multiply -composite \) -alpha off -compose CopyOpacity -composite $1.1tmp.png +convert -border 10x10 -bordercolor none $1.1tmp.png $1.2tmp.png +convert $1.2tmp.png -background "#4760e9" -gravity south -extent 2208x1242 $1.3tmp.png +convert $1.3tmp.png -gravity North -pointsize 80 -font Roboto-Condensed-Regular -fill "#ffffff" -annotate +0+10 "$2" $1.4tmp.png \ No newline at end of file diff --git a/resources/metadata/en-US/images/featureGraphic.svg b/resources/metadata/en-US/images/featureGraphic.svg new file mode 100644 index 00000000..10c872e8 --- /dev/null +++ b/resources/metadata/en-US/images/featureGraphic.svg @@ -0,0 +1,12 @@ + + + + Play music Hit every single note + + + Hit Notes + + + + + diff --git a/resources/metadata/en-US/images/icon.svg b/resources/metadata/en-US/images/icon.svg new file mode 100644 index 00000000..4d9f380a --- /dev/null +++ b/resources/metadata/en-US/images/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/metadata/en-US/images/phoneScreenshots/1_en-US.png b/resources/metadata/en-US/images/phoneScreenshots/1_en-US.png new file mode 100644 index 00000000..8e3e907b Binary files /dev/null and b/resources/metadata/en-US/images/phoneScreenshots/1_en-US.png differ diff --git a/resources/metadata/en-US/images/phoneScreenshots/1_en-US.txt b/resources/metadata/en-US/images/phoneScreenshots/1_en-US.txt new file mode 100644 index 00000000..560eae40 --- /dev/null +++ b/resources/metadata/en-US/images/phoneScreenshots/1_en-US.txt @@ -0,0 +1 @@ +Hundreds of songs and will be updated continuously diff --git a/resources/metadata/en-US/images/phoneScreenshots/2_en-US.png b/resources/metadata/en-US/images/phoneScreenshots/2_en-US.png new file mode 100644 index 00000000..74afd4a8 Binary files /dev/null and b/resources/metadata/en-US/images/phoneScreenshots/2_en-US.png differ diff --git a/resources/metadata/en-US/images/phoneScreenshots/2_en-US.txt b/resources/metadata/en-US/images/phoneScreenshots/2_en-US.txt new file mode 100644 index 00000000..1dbd0db0 --- /dev/null +++ b/resources/metadata/en-US/images/phoneScreenshots/2_en-US.txt @@ -0,0 +1 @@ +Many musical instruments diff --git a/resources/metadata/en-US/images/phoneScreenshots/3_en-US.png b/resources/metadata/en-US/images/phoneScreenshots/3_en-US.png new file mode 100644 index 00000000..0b07d0e8 Binary files /dev/null and b/resources/metadata/en-US/images/phoneScreenshots/3_en-US.png differ diff --git a/resources/metadata/en-US/images/phoneScreenshots/3_en-US.txt b/resources/metadata/en-US/images/phoneScreenshots/3_en-US.txt new file mode 100644 index 00000000..34a00f30 --- /dev/null +++ b/resources/metadata/en-US/images/phoneScreenshots/3_en-US.txt @@ -0,0 +1 @@ +Hit notes to play and listen diff --git a/resources/metadata/vi/images/featureGraphic.svg b/resources/metadata/vi/images/featureGraphic.svg new file mode 100644 index 00000000..04f227e1 --- /dev/null +++ b/resources/metadata/vi/images/featureGraphic.svg @@ -0,0 +1,12 @@ + + + + Chơi nhạc Chạm vào từng nốt + + + Hit Notes + + + + + diff --git a/resources/metadata/vi/images/icon.svg b/resources/metadata/vi/images/icon.svg new file mode 100644 index 00000000..4d9f380a --- /dev/null +++ b/resources/metadata/vi/images/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/metadata/vi/images/phoneScreenshots/1_vi.png b/resources/metadata/vi/images/phoneScreenshots/1_vi.png new file mode 100644 index 00000000..949a8159 Binary files /dev/null and b/resources/metadata/vi/images/phoneScreenshots/1_vi.png differ diff --git a/resources/metadata/vi/images/phoneScreenshots/1_vi.txt b/resources/metadata/vi/images/phoneScreenshots/1_vi.txt new file mode 100644 index 00000000..ab9ce294 --- /dev/null +++ b/resources/metadata/vi/images/phoneScreenshots/1_vi.txt @@ -0,0 +1 @@ +Hàng trăm bản nhạc và sẽ liên tục được cập nhật diff --git a/resources/metadata/vi/images/phoneScreenshots/2_vi.png b/resources/metadata/vi/images/phoneScreenshots/2_vi.png new file mode 100644 index 00000000..53de64cb Binary files /dev/null and b/resources/metadata/vi/images/phoneScreenshots/2_vi.png differ diff --git a/resources/metadata/vi/images/phoneScreenshots/2_vi.txt b/resources/metadata/vi/images/phoneScreenshots/2_vi.txt new file mode 100644 index 00000000..74e0a4aa --- /dev/null +++ b/resources/metadata/vi/images/phoneScreenshots/2_vi.txt @@ -0,0 +1 @@ +Nhiều nhạc cụ diff --git a/resources/metadata/vi/images/phoneScreenshots/3_vi.png b/resources/metadata/vi/images/phoneScreenshots/3_vi.png new file mode 100644 index 00000000..ac86e66a Binary files /dev/null and b/resources/metadata/vi/images/phoneScreenshots/3_vi.png differ diff --git a/resources/metadata/vi/images/phoneScreenshots/3_vi.txt b/resources/metadata/vi/images/phoneScreenshots/3_vi.txt new file mode 100644 index 00000000..53f5a59f --- /dev/null +++ b/resources/metadata/vi/images/phoneScreenshots/3_vi.txt @@ -0,0 +1 @@ +Chạm vào các nốt để chơi và lắng nghe diff --git a/resources/util.py b/resources/util.py new file mode 100644 index 00000000..f86967c1 --- /dev/null +++ b/resources/util.py @@ -0,0 +1,22 @@ +import io +import os +import re + + +def grep(regex, base_dir, extension, skip_files=[]): + used_resources = set() + for root, _, files in os.walk(base_dir): + for file in files: + if file.lower().endswith(extension): + if file in skip_files: + continue + try: + with io.open(os.path.join(root, file), 'r', encoding='utf8') as f: + content = f.read() + match_list = re.findall(regex, content) + if match_list: + for i in match_list: + used_resources.add(i) + except UnicodeDecodeError: + print('Cannot read {0} as text'.format(os.path.join(root, file))) + return used_resources diff --git a/resources/validate_functions.py b/resources/validate_functions.py new file mode 100644 index 00000000..3b511f9c --- /dev/null +++ b/resources/validate_functions.py @@ -0,0 +1,11 @@ +from util import grep + +used_functions = list(map(lambda x: x.replace('getHttpsCallable("', '').replace('"', ''), + sorted(grep("getHttpsCallable\(\".*?\"", "../client", ('.kt', '.java'))))) +functions = list(map(lambda x: x.replace('exports.', '').replace(' = functions', ''), + sorted(grep("exports.*? = functions", "../functions/functions/src", '.ts')))) + +diff = [item for item in used_functions if item not in functions] + [item for item in functions if + item not in used_functions and item != 'onUserDeleted' and item != 'onUserSignUp' and item != 'checkIfExpiredSubscriptionsRenewed' and item != 'sendNewSongNotification'] +if len(diff) != 0: + raise Exception(diff) diff --git a/resources/validate_images.py b/resources/validate_images.py new file mode 100644 index 00000000..d07becfd --- /dev/null +++ b/resources/validate_images.py @@ -0,0 +1,12 @@ +import os + +from util import grep + +used_textures = list(map(lambda x: x.replace('\'', '').replace('.png', ''), + sorted(grep("img_.*\'", "../lib", '.dart')))) +texture_files = list(map(lambda x: x.replace('.svg', ''), + sorted(os.listdir('images')))) +diff = [item for item in texture_files if item not in used_textures] + [item for item in used_textures if + item not in texture_files] +if len(diff) != 0: + raise Exception(diff) diff --git a/resources/validate_strings.py b/resources/validate_strings.py new file mode 100644 index 00000000..92350b80 --- /dev/null +++ b/resources/validate_strings.py @@ -0,0 +1,8 @@ +from util import grep + +used_strings = list(map(lambda x: x.replace('.', '').replace(')', '').replace(',', '').replace(';', '').split('(', 1)[0], + sorted(grep('\.txt_.*?\,', "../lib", '.dart')) + sorted(grep('\.txt_.*?\;', "../lib", '.dart'))+ sorted(grep('\.txt_.*?\)', "../lib", '.dart')))) +strings = list(map(lambda x: x.replace('"', ''), sorted(grep("\"txt_.*?\"", "../lib/l10n", '.arb')))) +diff = [item for item in used_strings if item not in strings] + [item for item in strings if item not in used_strings] +if len(diff) != 0: + raise Exception(diff) diff --git a/storage/songs/Vietnamese_Folk-A_Test_Song.mid b/storage/songs/Vietnamese_Folk-A_Test_Song.mid new file mode 100644 index 00000000..a9e7630b Binary files /dev/null and b/storage/songs/Vietnamese_Folk-A_Test_Song.mid differ diff --git a/storage/songs/classic/Aram_Khachaturian-Masquerade_Waltz.mid b/storage/songs/classic/Aram_Khachaturian-Masquerade_Waltz.mid new file mode 100644 index 00000000..e873ff91 Binary files /dev/null and b/storage/songs/classic/Aram_Khachaturian-Masquerade_Waltz.mid differ diff --git a/storage/songs/classic/Aram_Khachaturian-Sabr_Dance.mid b/storage/songs/classic/Aram_Khachaturian-Sabr_Dance.mid new file mode 100644 index 00000000..edd8c46c Binary files /dev/null and b/storage/songs/classic/Aram_Khachaturian-Sabr_Dance.mid differ diff --git a/storage/songs/classic/Bach-Air_On_The_G_String.mid b/storage/songs/classic/Bach-Air_On_The_G_String.mid new file mode 100644 index 00000000..adcb68ba Binary files /dev/null and b/storage/songs/classic/Bach-Air_On_The_G_String.mid differ diff --git a/storage/songs/classic/Bach-Badinerie.mid b/storage/songs/classic/Bach-Badinerie.mid new file mode 100644 index 00000000..b817cb6b Binary files /dev/null and b/storage/songs/classic/Bach-Badinerie.mid differ diff --git a/storage/songs/classic/Bach-Bourree_In_E_Minor.mid b/storage/songs/classic/Bach-Bourree_In_E_Minor.mid new file mode 100644 index 00000000..06d3eb61 Binary files /dev/null and b/storage/songs/classic/Bach-Bourree_In_E_Minor.mid differ diff --git a/storage/songs/classic/Bach-Cello_Suite.mid b/storage/songs/classic/Bach-Cello_Suite.mid new file mode 100644 index 00000000..dc8a588f Binary files /dev/null and b/storage/songs/classic/Bach-Cello_Suite.mid differ diff --git a/storage/songs/classic/Bach-Joy.mid b/storage/songs/classic/Bach-Joy.mid new file mode 100644 index 00000000..a75686ce Binary files /dev/null and b/storage/songs/classic/Bach-Joy.mid differ diff --git a/storage/songs/classic/Bach-Minuet_In_G.mid b/storage/songs/classic/Bach-Minuet_In_G.mid new file mode 100644 index 00000000..77c646d5 Binary files /dev/null and b/storage/songs/classic/Bach-Minuet_In_G.mid differ diff --git a/storage/songs/classic/Bach-Toccata_and_Fugue.mid b/storage/songs/classic/Bach-Toccata_and_Fugue.mid new file mode 100644 index 00000000..760db054 Binary files /dev/null and b/storage/songs/classic/Bach-Toccata_and_Fugue.mid differ diff --git a/storage/songs/classic/Bach-Well_Tempered_Clavier.mid b/storage/songs/classic/Bach-Well_Tempered_Clavier.mid new file mode 100644 index 00000000..549d5a94 Binary files /dev/null and b/storage/songs/classic/Bach-Well_Tempered_Clavier.mid differ diff --git a/storage/songs/classic/Beethoven-Appasionata_Sonate.mid b/storage/songs/classic/Beethoven-Appasionata_Sonate.mid new file mode 100644 index 00000000..f57192b9 Binary files /dev/null and b/storage/songs/classic/Beethoven-Appasionata_Sonate.mid differ diff --git a/storage/songs/classic/Beethoven-Fur_Elise.mid b/storage/songs/classic/Beethoven-Fur_Elise.mid new file mode 100644 index 00000000..2da50399 Binary files /dev/null and b/storage/songs/classic/Beethoven-Fur_Elise.mid differ diff --git a/storage/songs/classic/Beethoven-Moonlight_Sonata_Mvt._1.mid b/storage/songs/classic/Beethoven-Moonlight_Sonata_Mvt._1.mid new file mode 100644 index 00000000..33b7fe4b Binary files /dev/null and b/storage/songs/classic/Beethoven-Moonlight_Sonata_Mvt._1.mid differ diff --git a/storage/songs/classic/Beethoven-Moonlight_Sonata_Mvt._3.mid b/storage/songs/classic/Beethoven-Moonlight_Sonata_Mvt._3.mid new file mode 100644 index 00000000..5b745a93 Binary files /dev/null and b/storage/songs/classic/Beethoven-Moonlight_Sonata_Mvt._3.mid differ diff --git a/storage/songs/classic/Beethoven-String_Quartet.mid b/storage/songs/classic/Beethoven-String_Quartet.mid new file mode 100644 index 00000000..1bdbdb94 Binary files /dev/null and b/storage/songs/classic/Beethoven-String_Quartet.mid differ diff --git a/storage/songs/classic/Beethoven-Symphony_No._3_Mvt._1.mid b/storage/songs/classic/Beethoven-Symphony_No._3_Mvt._1.mid new file mode 100644 index 00000000..7445caec Binary files /dev/null and b/storage/songs/classic/Beethoven-Symphony_No._3_Mvt._1.mid differ diff --git a/storage/songs/classic/Beethoven-Symphony_No._5_Mvt._1.mid b/storage/songs/classic/Beethoven-Symphony_No._5_Mvt._1.mid new file mode 100644 index 00000000..6133751a Binary files /dev/null and b/storage/songs/classic/Beethoven-Symphony_No._5_Mvt._1.mid differ diff --git a/storage/songs/classic/Beethoven-Symphony_No._5_Mvt._3.mid b/storage/songs/classic/Beethoven-Symphony_No._5_Mvt._3.mid new file mode 100644 index 00000000..cf284d83 Binary files /dev/null and b/storage/songs/classic/Beethoven-Symphony_No._5_Mvt._3.mid differ diff --git a/storage/songs/classic/Beethoven-Symphony_No._6.mid b/storage/songs/classic/Beethoven-Symphony_No._6.mid new file mode 100644 index 00000000..c16fbebd Binary files /dev/null and b/storage/songs/classic/Beethoven-Symphony_No._6.mid differ diff --git a/storage/songs/classic/Beethoven-Symphony_No._7_Mvt._2.mid b/storage/songs/classic/Beethoven-Symphony_No._7_Mvt._2.mid new file mode 100644 index 00000000..a384ee1d Binary files /dev/null and b/storage/songs/classic/Beethoven-Symphony_No._7_Mvt._2.mid differ diff --git a/storage/songs/classic/Beethoven-Symphony_No._9_Mvt._2.mid b/storage/songs/classic/Beethoven-Symphony_No._9_Mvt._2.mid new file mode 100644 index 00000000..2d35ada6 Binary files /dev/null and b/storage/songs/classic/Beethoven-Symphony_No._9_Mvt._2.mid differ diff --git a/storage/songs/classic/Beethoven-Symphony_No._9_Mvt._4.mid b/storage/songs/classic/Beethoven-Symphony_No._9_Mvt._4.mid new file mode 100644 index 00000000..79d009c7 Binary files /dev/null and b/storage/songs/classic/Beethoven-Symphony_No._9_Mvt._4.mid differ diff --git a/storage/songs/classic/Beethoven-Turkish_March.mid b/storage/songs/classic/Beethoven-Turkish_March.mid new file mode 100644 index 00000000..91b479ba Binary files /dev/null and b/storage/songs/classic/Beethoven-Turkish_March.mid differ diff --git a/storage/songs/classic/Carl_Orff-O_Fortuna.mid b/storage/songs/classic/Carl_Orff-O_Fortuna.mid new file mode 100644 index 00000000..e3d8bdc3 Binary files /dev/null and b/storage/songs/classic/Carl_Orff-O_Fortuna.mid differ diff --git a/storage/songs/classic/Chopin-Etude_Op._10_No._3.mid b/storage/songs/classic/Chopin-Etude_Op._10_No._3.mid new file mode 100644 index 00000000..5d2ebd01 Binary files /dev/null and b/storage/songs/classic/Chopin-Etude_Op._10_No._3.mid differ diff --git a/storage/songs/classic/Chopin-Fantaisie_Impromptu.mid b/storage/songs/classic/Chopin-Fantaisie_Impromptu.mid new file mode 100644 index 00000000..3253146c Binary files /dev/null and b/storage/songs/classic/Chopin-Fantaisie_Impromptu.mid differ diff --git a/storage/songs/classic/Chopin-Funeral_March.mid b/storage/songs/classic/Chopin-Funeral_March.mid new file mode 100644 index 00000000..8086bc07 Binary files /dev/null and b/storage/songs/classic/Chopin-Funeral_March.mid differ diff --git a/storage/songs/classic/Chopin-Heroic_Polonaise.mid b/storage/songs/classic/Chopin-Heroic_Polonaise.mid new file mode 100644 index 00000000..e8793479 Binary files /dev/null and b/storage/songs/classic/Chopin-Heroic_Polonaise.mid differ diff --git a/storage/songs/classic/Chopin-Minute_Waltz.mid b/storage/songs/classic/Chopin-Minute_Waltz.mid new file mode 100644 index 00000000..4a25e626 Binary files /dev/null and b/storage/songs/classic/Chopin-Minute_Waltz.mid differ diff --git a/storage/songs/classic/Chopin-Nocturne_Op._9_No._2.mid b/storage/songs/classic/Chopin-Nocturne_Op._9_No._2.mid new file mode 100644 index 00000000..63f9b5e8 Binary files /dev/null and b/storage/songs/classic/Chopin-Nocturne_Op._9_No._2.mid differ diff --git a/storage/songs/classic/Chopin-Revolutionary_Etude.mid b/storage/songs/classic/Chopin-Revolutionary_Etude.mid new file mode 100644 index 00000000..f521257c Binary files /dev/null and b/storage/songs/classic/Chopin-Revolutionary_Etude.mid differ diff --git a/storage/songs/classic/Chopin-Waltz_Op._64_No._2.mid b/storage/songs/classic/Chopin-Waltz_Op._64_No._2.mid new file mode 100644 index 00000000..ecc548ba Binary files /dev/null and b/storage/songs/classic/Chopin-Waltz_Op._64_No._2.mid differ diff --git a/storage/songs/classic/Chopin-Waltz_in_A_Minor.mid b/storage/songs/classic/Chopin-Waltz_in_A_Minor.mid new file mode 100644 index 00000000..3f6d9c3c Binary files /dev/null and b/storage/songs/classic/Chopin-Waltz_in_A_Minor.mid differ diff --git a/storage/songs/classic/Debussy-Clair_De_Lune.mid b/storage/songs/classic/Debussy-Clair_De_Lune.mid new file mode 100644 index 00000000..e758d66b Binary files /dev/null and b/storage/songs/classic/Debussy-Clair_De_Lune.mid differ diff --git a/storage/songs/classic/Delibes-Flower_Duet.mid b/storage/songs/classic/Delibes-Flower_Duet.mid new file mode 100644 index 00000000..ae555bb4 Binary files /dev/null and b/storage/songs/classic/Delibes-Flower_Duet.mid differ diff --git a/storage/songs/classic/Delibes-Pizzicato.mid b/storage/songs/classic/Delibes-Pizzicato.mid new file mode 100644 index 00000000..933b4891 Binary files /dev/null and b/storage/songs/classic/Delibes-Pizzicato.mid differ diff --git a/storage/songs/classic/Dmitri_Shostakovich-Second_Waltz.mid b/storage/songs/classic/Dmitri_Shostakovich-Second_Waltz.mid new file mode 100644 index 00000000..0de9f58a Binary files /dev/null and b/storage/songs/classic/Dmitri_Shostakovich-Second_Waltz.mid differ diff --git a/storage/songs/classic/Dvorak-Humoresque_No._7.mid b/storage/songs/classic/Dvorak-Humoresque_No._7.mid new file mode 100644 index 00000000..d8674a84 Binary files /dev/null and b/storage/songs/classic/Dvorak-Humoresque_No._7.mid differ diff --git a/storage/songs/classic/Dvorak-Slavonic_Dance_No._2.mid b/storage/songs/classic/Dvorak-Slavonic_Dance_No._2.mid new file mode 100644 index 00000000..4441170c Binary files /dev/null and b/storage/songs/classic/Dvorak-Slavonic_Dance_No._2.mid differ diff --git a/storage/songs/classic/Dvorak-Symphony_No._9_Mvt._4.mid b/storage/songs/classic/Dvorak-Symphony_No._9_Mvt._4.mid new file mode 100644 index 00000000..d1556eb5 Binary files /dev/null and b/storage/songs/classic/Dvorak-Symphony_No._9_Mvt._4.mid differ diff --git a/storage/songs/classic/Edvard_Grieg-Anitras_Dance.mid b/storage/songs/classic/Edvard_Grieg-Anitras_Dance.mid new file mode 100644 index 00000000..1dc8abec Binary files /dev/null and b/storage/songs/classic/Edvard_Grieg-Anitras_Dance.mid differ diff --git a/storage/songs/classic/Edvard_Grieg-Morning_Mood.mid b/storage/songs/classic/Edvard_Grieg-Morning_Mood.mid new file mode 100644 index 00000000..f9d0e648 Binary files /dev/null and b/storage/songs/classic/Edvard_Grieg-Morning_Mood.mid differ diff --git a/storage/songs/classic/Edvard_Grieg-Piano_Concerto.mid b/storage/songs/classic/Edvard_Grieg-Piano_Concerto.mid new file mode 100644 index 00000000..820b4f3c Binary files /dev/null and b/storage/songs/classic/Edvard_Grieg-Piano_Concerto.mid differ diff --git a/storage/songs/classic/Edward_Elgar-Pomp_And_Circumstance_March.mid b/storage/songs/classic/Edward_Elgar-Pomp_And_Circumstance_March.mid new file mode 100644 index 00000000..d337dddf Binary files /dev/null and b/storage/songs/classic/Edward_Elgar-Pomp_And_Circumstance_March.mid differ diff --git a/storage/songs/classic/Eugen_Doga-My_Sweet_And_Tender_Beast.mid b/storage/songs/classic/Eugen_Doga-My_Sweet_And_Tender_Beast.mid new file mode 100644 index 00000000..6c606ffc Binary files /dev/null and b/storage/songs/classic/Eugen_Doga-My_Sweet_And_Tender_Beast.mid differ diff --git a/storage/songs/classic/Franz_Liszt-Hungarian_Rhapsody_No._2.mid b/storage/songs/classic/Franz_Liszt-Hungarian_Rhapsody_No._2.mid new file mode 100644 index 00000000..1b8ad179 Binary files /dev/null and b/storage/songs/classic/Franz_Liszt-Hungarian_Rhapsody_No._2.mid differ diff --git a/storage/songs/classic/George_Gershwin-Rhapsody_In_Blue.mid b/storage/songs/classic/George_Gershwin-Rhapsody_In_Blue.mid new file mode 100644 index 00000000..8d32efe3 Binary files /dev/null and b/storage/songs/classic/George_Gershwin-Rhapsody_In_Blue.mid differ diff --git a/storage/songs/classic/Georges_Bizet-Habanera.mid b/storage/songs/classic/Georges_Bizet-Habanera.mid new file mode 100644 index 00000000..0dfc8551 Binary files /dev/null and b/storage/songs/classic/Georges_Bizet-Habanera.mid differ diff --git a/storage/songs/classic/Georges_Bizet-Les_Toradors.mid b/storage/songs/classic/Georges_Bizet-Les_Toradors.mid new file mode 100644 index 00000000..09571a06 Binary files /dev/null and b/storage/songs/classic/Georges_Bizet-Les_Toradors.mid differ diff --git a/storage/songs/classic/Georges_Bizet-March_Of_The_Toreadors.mid b/storage/songs/classic/Georges_Bizet-March_Of_The_Toreadors.mid new file mode 100644 index 00000000..b5c7bcec Binary files /dev/null and b/storage/songs/classic/Georges_Bizet-March_Of_The_Toreadors.mid differ diff --git a/storage/songs/classic/Giacomo_Puccini-Madama_Butterfly.mid b/storage/songs/classic/Giacomo_Puccini-Madama_Butterfly.mid new file mode 100644 index 00000000..d55ad8a5 Binary files /dev/null and b/storage/songs/classic/Giacomo_Puccini-Madama_Butterfly.mid differ diff --git a/storage/songs/classic/Giacomo_Puccini-Nessun_Dorma.mid b/storage/songs/classic/Giacomo_Puccini-Nessun_Dorma.mid new file mode 100644 index 00000000..ff95a916 Binary files /dev/null and b/storage/songs/classic/Giacomo_Puccini-Nessun_Dorma.mid differ diff --git a/storage/songs/classic/Giuseppe_Verdi-Drinking_Song.mid b/storage/songs/classic/Giuseppe_Verdi-Drinking_Song.mid new file mode 100644 index 00000000..bd085b61 Binary files /dev/null and b/storage/songs/classic/Giuseppe_Verdi-Drinking_Song.mid differ diff --git a/storage/songs/classic/Giuseppe_Verdi-La_Donna_E_Mobile.mid b/storage/songs/classic/Giuseppe_Verdi-La_Donna_E_Mobile.mid new file mode 100644 index 00000000..db5dccf6 Binary files /dev/null and b/storage/songs/classic/Giuseppe_Verdi-La_Donna_E_Mobile.mid differ diff --git a/storage/songs/classic/Giuseppe_Verdi-Triumphal_March.mid b/storage/songs/classic/Giuseppe_Verdi-Triumphal_March.mid new file mode 100644 index 00000000..906d5a2f Binary files /dev/null and b/storage/songs/classic/Giuseppe_Verdi-Triumphal_March.mid differ diff --git a/storage/songs/classic/Gounod-Funeral_March_Of_A_Marionette.mid b/storage/songs/classic/Gounod-Funeral_March_Of_A_Marionette.mid new file mode 100644 index 00000000..6b4b2de6 Binary files /dev/null and b/storage/songs/classic/Gounod-Funeral_March_Of_A_Marionette.mid differ diff --git a/storage/songs/classic/Gustav_Mahler-Symphony_No._5_Mvt._4.mid b/storage/songs/classic/Gustav_Mahler-Symphony_No._5_Mvt._4.mid new file mode 100644 index 00000000..26776492 Binary files /dev/null and b/storage/songs/classic/Gustav_Mahler-Symphony_No._5_Mvt._4.mid differ diff --git a/storage/songs/classic/Handel-Arrival_Of_The_Queen_Of_Sheba.mid b/storage/songs/classic/Handel-Arrival_Of_The_Queen_Of_Sheba.mid new file mode 100644 index 00000000..ab4022ad Binary files /dev/null and b/storage/songs/classic/Handel-Arrival_Of_The_Queen_Of_Sheba.mid differ diff --git a/storage/songs/classic/Handel-Hallelujah_Chorus.mid b/storage/songs/classic/Handel-Hallelujah_Chorus.mid new file mode 100644 index 00000000..387d9840 Binary files /dev/null and b/storage/songs/classic/Handel-Hallelujah_Chorus.mid differ diff --git a/storage/songs/classic/Handel-La_Rejouissance.mid b/storage/songs/classic/Handel-La_Rejouissance.mid new file mode 100644 index 00000000..1ed3e146 Binary files /dev/null and b/storage/songs/classic/Handel-La_Rejouissance.mid differ diff --git a/storage/songs/classic/Handel-Ombra_Mai_Fu.mid b/storage/songs/classic/Handel-Ombra_Mai_Fu.mid new file mode 100644 index 00000000..2a376b44 Binary files /dev/null and b/storage/songs/classic/Handel-Ombra_Mai_Fu.mid differ diff --git a/storage/songs/classic/Handel-Sarabande.mid b/storage/songs/classic/Handel-Sarabande.mid new file mode 100644 index 00000000..680bedb7 Binary files /dev/null and b/storage/songs/classic/Handel-Sarabande.mid differ diff --git a/storage/songs/classic/Handel-Zadok_The_Priest.mid b/storage/songs/classic/Handel-Zadok_The_Priest.mid new file mode 100644 index 00000000..c6085f2d Binary files /dev/null and b/storage/songs/classic/Handel-Zadok_The_Priest.mid differ diff --git a/storage/songs/classic/Haydn-The_Nutcracker_-_Russian_Dance.mid b/storage/songs/classic/Haydn-The_Nutcracker_-_Russian_Dance.mid new file mode 100644 index 00000000..8b3aea71 Binary files /dev/null and b/storage/songs/classic/Haydn-The_Nutcracker_-_Russian_Dance.mid differ diff --git a/storage/songs/classic/Haydn-Trumpet_Concerto.mid b/storage/songs/classic/Haydn-Trumpet_Concerto.mid new file mode 100644 index 00000000..6cff7c54 Binary files /dev/null and b/storage/songs/classic/Haydn-Trumpet_Concerto.mid differ diff --git a/storage/songs/classic/Jacques_Offenbach-Barcarolle.mid b/storage/songs/classic/Jacques_Offenbach-Barcarolle.mid new file mode 100644 index 00000000..fd2d0449 Binary files /dev/null and b/storage/songs/classic/Jacques_Offenbach-Barcarolle.mid differ diff --git a/storage/songs/classic/Jacques_Offenbach-Orpheus_In_The_Underworld.mid b/storage/songs/classic/Jacques_Offenbach-Orpheus_In_The_Underworld.mid new file mode 100644 index 00000000..4ae35edd Binary files /dev/null and b/storage/songs/classic/Jacques_Offenbach-Orpheus_In_The_Underworld.mid differ diff --git a/storage/songs/classic/Johann_Strauss-Blue_Danube_Waltz.mid b/storage/songs/classic/Johann_Strauss-Blue_Danube_Waltz.mid new file mode 100644 index 00000000..5f363230 Binary files /dev/null and b/storage/songs/classic/Johann_Strauss-Blue_Danube_Waltz.mid differ diff --git a/storage/songs/classic/Johann_Strauss-Die_Fledermaus.mid b/storage/songs/classic/Johann_Strauss-Die_Fledermaus.mid new file mode 100644 index 00000000..757c1e6c Binary files /dev/null and b/storage/songs/classic/Johann_Strauss-Die_Fledermaus.mid differ diff --git a/storage/songs/classic/Johann_Strauss-Emperor_Waltz.mid b/storage/songs/classic/Johann_Strauss-Emperor_Waltz.mid new file mode 100644 index 00000000..af3da2ec Binary files /dev/null and b/storage/songs/classic/Johann_Strauss-Emperor_Waltz.mid differ diff --git a/storage/songs/classic/Johann_Strauss-Radetzky_March.mid b/storage/songs/classic/Johann_Strauss-Radetzky_March.mid new file mode 100644 index 00000000..46bb5e4c Binary files /dev/null and b/storage/songs/classic/Johann_Strauss-Radetzky_March.mid differ diff --git a/storage/songs/classic/Johann_Strauss-Tritsch_Tratsch_Polka.mid b/storage/songs/classic/Johann_Strauss-Tritsch_Tratsch_Polka.mid new file mode 100644 index 00000000..e601a42d Binary files /dev/null and b/storage/songs/classic/Johann_Strauss-Tritsch_Tratsch_Polka.mid differ diff --git a/storage/songs/classic/Johann_Strauss-Voices_Of_Spring.mid b/storage/songs/classic/Johann_Strauss-Voices_Of_Spring.mid new file mode 100644 index 00000000..8187e6a4 Binary files /dev/null and b/storage/songs/classic/Johann_Strauss-Voices_Of_Spring.mid differ diff --git a/storage/songs/classic/Johann_Strauss-Wiener_Blut.mid b/storage/songs/classic/Johann_Strauss-Wiener_Blut.mid new file mode 100644 index 00000000..56f670d6 Binary files /dev/null and b/storage/songs/classic/Johann_Strauss-Wiener_Blut.mid differ diff --git a/storage/songs/classic/Johannes_Brahms-Hungarian_Dance_No_5.mid b/storage/songs/classic/Johannes_Brahms-Hungarian_Dance_No_5.mid new file mode 100644 index 00000000..f0bf8d9d Binary files /dev/null and b/storage/songs/classic/Johannes_Brahms-Hungarian_Dance_No_5.mid differ diff --git a/storage/songs/classic/Johannes_Brahms-Hungarian_Dance_No_6.mid b/storage/songs/classic/Johannes_Brahms-Hungarian_Dance_No_6.mid new file mode 100644 index 00000000..09b20316 Binary files /dev/null and b/storage/songs/classic/Johannes_Brahms-Hungarian_Dance_No_6.mid differ diff --git a/storage/songs/classic/John_Philip_Sousa-Liberty_Bell.mid b/storage/songs/classic/John_Philip_Sousa-Liberty_Bell.mid new file mode 100644 index 00000000..c8af7c42 Binary files /dev/null and b/storage/songs/classic/John_Philip_Sousa-Liberty_Bell.mid differ diff --git a/storage/songs/classic/John_Philip_Sousa-Semper_Fidelis.mid b/storage/songs/classic/John_Philip_Sousa-Semper_Fidelis.mid new file mode 100644 index 00000000..1e866965 Binary files /dev/null and b/storage/songs/classic/John_Philip_Sousa-Semper_Fidelis.mid differ diff --git a/storage/songs/classic/John_Philip_Sousa-Stars_And_Stripes_Forever.mid b/storage/songs/classic/John_Philip_Sousa-Stars_And_Stripes_Forever.mid new file mode 100644 index 00000000..03294f69 Binary files /dev/null and b/storage/songs/classic/John_Philip_Sousa-Stars_And_Stripes_Forever.mid differ diff --git a/storage/songs/classic/Julius_Fucik-Entry_Of_The_Gladiators.mid b/storage/songs/classic/Julius_Fucik-Entry_Of_The_Gladiators.mid new file mode 100644 index 00000000..07520cf4 Binary files /dev/null and b/storage/songs/classic/Julius_Fucik-Entry_Of_The_Gladiators.mid differ diff --git a/storage/songs/classic/Juventino_Rosas-Over_The_Waves.mid b/storage/songs/classic/Juventino_Rosas-Over_The_Waves.mid new file mode 100644 index 00000000..1b4fa315 Binary files /dev/null and b/storage/songs/classic/Juventino_Rosas-Over_The_Waves.mid differ diff --git a/storage/songs/classic/Karl_Jenkins-Palladio.mid b/storage/songs/classic/Karl_Jenkins-Palladio.mid new file mode 100644 index 00000000..63397fa0 Binary files /dev/null and b/storage/songs/classic/Karl_Jenkins-Palladio.mid differ diff --git a/storage/songs/classic/Ketelbey-In_A_Persian_Market.mid b/storage/songs/classic/Ketelbey-In_A_Persian_Market.mid new file mode 100644 index 00000000..8146120f Binary files /dev/null and b/storage/songs/classic/Ketelbey-In_A_Persian_Market.mid differ diff --git a/storage/songs/classic/Liszt-Etude_In_G_Minor_La_Campanella.mid b/storage/songs/classic/Liszt-Etude_In_G_Minor_La_Campanella.mid new file mode 100644 index 00000000..e9472f00 Binary files /dev/null and b/storage/songs/classic/Liszt-Etude_In_G_Minor_La_Campanella.mid differ diff --git a/storage/songs/classic/Liszt-Liebestraum_No._3.mid b/storage/songs/classic/Liszt-Liebestraum_No._3.mid new file mode 100644 index 00000000..cb198fb7 Binary files /dev/null and b/storage/songs/classic/Liszt-Liebestraum_No._3.mid differ diff --git a/storage/songs/classic/Luigi_Denza-Funiculi_Funicula.mid b/storage/songs/classic/Luigi_Denza-Funiculi_Funicula.mid new file mode 100644 index 00000000..0388861d Binary files /dev/null and b/storage/songs/classic/Luigi_Denza-Funiculi_Funicula.mid differ diff --git a/storage/songs/classic/Mendelssohn_Bartholdy-Wedding_March.mid b/storage/songs/classic/Mendelssohn_Bartholdy-Wedding_March.mid new file mode 100644 index 00000000..e0c8769e Binary files /dev/null and b/storage/songs/classic/Mendelssohn_Bartholdy-Wedding_March.mid differ diff --git a/storage/songs/classic/Mozart-Aria_Queen_Of_The_Night.mid b/storage/songs/classic/Mozart-Aria_Queen_Of_The_Night.mid new file mode 100644 index 00000000..3f49b361 Binary files /dev/null and b/storage/songs/classic/Mozart-Aria_Queen_Of_The_Night.mid differ diff --git a/storage/songs/classic/Mozart-Eine_Kleine_Nachtmusik.mid b/storage/songs/classic/Mozart-Eine_Kleine_Nachtmusik.mid new file mode 100644 index 00000000..39e18c01 Binary files /dev/null and b/storage/songs/classic/Mozart-Eine_Kleine_Nachtmusik.mid differ diff --git a/storage/songs/classic/Mozart-Lacrimosa.mid b/storage/songs/classic/Mozart-Lacrimosa.mid new file mode 100644 index 00000000..fd418c39 Binary files /dev/null and b/storage/songs/classic/Mozart-Lacrimosa.mid differ diff --git a/storage/songs/classic/Mozart-Marriage_Of_Figaro.mid b/storage/songs/classic/Mozart-Marriage_Of_Figaro.mid new file mode 100644 index 00000000..be27225e Binary files /dev/null and b/storage/songs/classic/Mozart-Marriage_Of_Figaro.mid differ diff --git a/storage/songs/classic/Mozart-Piano_Sonata_No._11.mid b/storage/songs/classic/Mozart-Piano_Sonata_No._11.mid new file mode 100644 index 00000000..b73f55d2 Binary files /dev/null and b/storage/songs/classic/Mozart-Piano_Sonata_No._11.mid differ diff --git a/storage/songs/classic/Mozart-Requiem.mid b/storage/songs/classic/Mozart-Requiem.mid new file mode 100644 index 00000000..3f3dbe0a Binary files /dev/null and b/storage/songs/classic/Mozart-Requiem.mid differ diff --git a/storage/songs/classic/Mozart-Rondo_Alla_Turca.mid b/storage/songs/classic/Mozart-Rondo_Alla_Turca.mid new file mode 100644 index 00000000..20d1c35d Binary files /dev/null and b/storage/songs/classic/Mozart-Rondo_Alla_Turca.mid differ diff --git a/storage/songs/classic/Mozart-Symphony_No._25.mid b/storage/songs/classic/Mozart-Symphony_No._25.mid new file mode 100644 index 00000000..3364bdf0 Binary files /dev/null and b/storage/songs/classic/Mozart-Symphony_No._25.mid differ diff --git a/storage/songs/classic/Mozart-Symphony_No._40_in_G.mid b/storage/songs/classic/Mozart-Symphony_No._40_in_G.mid new file mode 100644 index 00000000..a4a8ce54 Binary files /dev/null and b/storage/songs/classic/Mozart-Symphony_No._40_in_G.mid differ diff --git a/storage/songs/classic/Mozart-Turkish_March.mid b/storage/songs/classic/Mozart-Turkish_March.mid new file mode 100644 index 00000000..0169b6be Binary files /dev/null and b/storage/songs/classic/Mozart-Turkish_March.mid differ diff --git a/storage/songs/classic/Paganini-Caprice_No._24.mid b/storage/songs/classic/Paganini-Caprice_No._24.mid new file mode 100644 index 00000000..5a657455 Binary files /dev/null and b/storage/songs/classic/Paganini-Caprice_No._24.mid differ diff --git a/storage/songs/classic/Ponchielli-Dance_Of_The_Hours.mid b/storage/songs/classic/Ponchielli-Dance_Of_The_Hours.mid new file mode 100644 index 00000000..9ce098ec Binary files /dev/null and b/storage/songs/classic/Ponchielli-Dance_Of_The_Hours.mid differ diff --git a/storage/songs/classic/Purcell-Funeral_Of_Queen_Mary.mid b/storage/songs/classic/Purcell-Funeral_Of_Queen_Mary.mid new file mode 100644 index 00000000..cec1f074 Binary files /dev/null and b/storage/songs/classic/Purcell-Funeral_Of_Queen_Mary.mid differ diff --git a/storage/songs/classic/Rachmaninoff-Prelude_In_C_Sharp_Minor.mid b/storage/songs/classic/Rachmaninoff-Prelude_In_C_Sharp_Minor.mid new file mode 100644 index 00000000..6f34a58b Binary files /dev/null and b/storage/songs/classic/Rachmaninoff-Prelude_In_C_Sharp_Minor.mid differ diff --git a/storage/songs/classic/Rachmaninoff-Rhapsody_On_A_Theme_Of_Paganini.mid b/storage/songs/classic/Rachmaninoff-Rhapsody_On_A_Theme_Of_Paganini.mid new file mode 100644 index 00000000..95f8af18 Binary files /dev/null and b/storage/songs/classic/Rachmaninoff-Rhapsody_On_A_Theme_Of_Paganini.mid differ diff --git a/storage/songs/classic/Respighi-Pines_Of_The_Appian_Way.mid b/storage/songs/classic/Respighi-Pines_Of_The_Appian_Way.mid new file mode 100644 index 00000000..fc2ef1f2 Binary files /dev/null and b/storage/songs/classic/Respighi-Pines_Of_The_Appian_Way.mid differ diff --git a/storage/songs/classic/Richard_Wagner-Tristan_And_Isolde.mid b/storage/songs/classic/Richard_Wagner-Tristan_And_Isolde.mid new file mode 100644 index 00000000..0f6dd939 Binary files /dev/null and b/storage/songs/classic/Richard_Wagner-Tristan_And_Isolde.mid differ diff --git a/storage/songs/classic/Ricketts-Colonel_Bogey_March.mid b/storage/songs/classic/Ricketts-Colonel_Bogey_March.mid new file mode 100644 index 00000000..cacc5806 Binary files /dev/null and b/storage/songs/classic/Ricketts-Colonel_Bogey_March.mid differ diff --git a/storage/songs/classic/Rimsky_Korsakov-Flight_Of_The_Bumblebee.mid b/storage/songs/classic/Rimsky_Korsakov-Flight_Of_The_Bumblebee.mid new file mode 100644 index 00000000..49255ae6 Binary files /dev/null and b/storage/songs/classic/Rimsky_Korsakov-Flight_Of_The_Bumblebee.mid differ diff --git a/storage/songs/classic/Rossini-Barber_Of_Seville_-_Overture.mid b/storage/songs/classic/Rossini-Barber_Of_Seville_-_Overture.mid new file mode 100644 index 00000000..004b7d35 Binary files /dev/null and b/storage/songs/classic/Rossini-Barber_Of_Seville_-_Overture.mid differ diff --git a/storage/songs/classic/Rossini-William_Tell_Overture_Finale.mid b/storage/songs/classic/Rossini-William_Tell_Overture_Finale.mid new file mode 100644 index 00000000..4076ab3b Binary files /dev/null and b/storage/songs/classic/Rossini-William_Tell_Overture_Finale.mid differ diff --git a/storage/songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Aquarium.mid b/storage/songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Aquarium.mid new file mode 100644 index 00000000..d88d13ee Binary files /dev/null and b/storage/songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Aquarium.mid differ diff --git a/storage/songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Finale.mid b/storage/songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Finale.mid new file mode 100644 index 00000000..88629b27 Binary files /dev/null and b/storage/songs/classic/Saint_Saens-Carnival_Of_The_Animals_-_Finale.mid differ diff --git a/storage/songs/classic/Saint_Saens-Danse_Macabre.mid b/storage/songs/classic/Saint_Saens-Danse_Macabre.mid new file mode 100644 index 00000000..154d1586 Binary files /dev/null and b/storage/songs/classic/Saint_Saens-Danse_Macabre.mid differ diff --git a/storage/songs/classic/Saint_Saens-Symphony_No._3.mid b/storage/songs/classic/Saint_Saens-Symphony_No._3.mid new file mode 100644 index 00000000..097b4d2f Binary files /dev/null and b/storage/songs/classic/Saint_Saens-Symphony_No._3.mid differ diff --git a/storage/songs/classic/Samuel_Barber-Adagio_For_Strings.mid b/storage/songs/classic/Samuel_Barber-Adagio_For_Strings.mid new file mode 100644 index 00000000..f2d0e856 Binary files /dev/null and b/storage/songs/classic/Samuel_Barber-Adagio_For_Strings.mid differ diff --git a/storage/songs/classic/Schubert-Ave_Maria.mid b/storage/songs/classic/Schubert-Ave_Maria.mid new file mode 100644 index 00000000..b3fc0523 Binary files /dev/null and b/storage/songs/classic/Schubert-Ave_Maria.mid differ diff --git a/storage/songs/classic/Smetana-Moldau_River.mid b/storage/songs/classic/Smetana-Moldau_River.mid new file mode 100644 index 00000000..7343718d Binary files /dev/null and b/storage/songs/classic/Smetana-Moldau_River.mid differ diff --git a/storage/songs/classic/Stravinsky-Firebird_Suite_-_Finale.mid b/storage/songs/classic/Stravinsky-Firebird_Suite_-_Finale.mid new file mode 100644 index 00000000..b07faa3b Binary files /dev/null and b/storage/songs/classic/Stravinsky-Firebird_Suite_-_Finale.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Dance_Of_The_Little_Swans.mid b/storage/songs/classic/Tchaikovsky-Dance_Of_The_Little_Swans.mid new file mode 100644 index 00000000..ce96a7f6 Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Dance_Of_The_Little_Swans.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Dance_Of_The_Reed_Flutes.mid b/storage/songs/classic/Tchaikovsky-Dance_Of_The_Reed_Flutes.mid new file mode 100644 index 00000000..4dcc47e3 Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Dance_Of_The_Reed_Flutes.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Dance_Of_The_Sugar_Plum_Fairy.mid b/storage/songs/classic/Tchaikovsky-Dance_Of_The_Sugar_Plum_Fairy.mid new file mode 100644 index 00000000..60a03985 Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Dance_Of_The_Sugar_Plum_Fairy.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Marche_Slave.mid b/storage/songs/classic/Tchaikovsky-Marche_Slave.mid new file mode 100644 index 00000000..f636910a Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Marche_Slave.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Nutcracker_-_March.mid b/storage/songs/classic/Tchaikovsky-Nutcracker_-_March.mid new file mode 100644 index 00000000..d9249c72 Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Nutcracker_-_March.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Piano_Concerto_No._1.mid b/storage/songs/classic/Tchaikovsky-Piano_Concerto_No._1.mid new file mode 100644 index 00000000..154cc2ee Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Piano_Concerto_No._1.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Romeo_And_Juliet_Fantasy_Overture.mid b/storage/songs/classic/Tchaikovsky-Romeo_And_Juliet_Fantasy_Overture.mid new file mode 100644 index 00000000..89e763ef Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Romeo_And_Juliet_Fantasy_Overture.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Sleeping_Beauty.mid b/storage/songs/classic/Tchaikovsky-Sleeping_Beauty.mid new file mode 100644 index 00000000..c5f5a7b6 Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Sleeping_Beauty.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Swan_Lake.mid b/storage/songs/classic/Tchaikovsky-Swan_Lake.mid new file mode 100644 index 00000000..30484161 Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Swan_Lake.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Symphony_No._6.mid b/storage/songs/classic/Tchaikovsky-Symphony_No._6.mid new file mode 100644 index 00000000..84fcf9d4 Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Symphony_No._6.mid differ diff --git a/storage/songs/classic/Tchaikovsky-Waltz_Of_The_Flowers.mid b/storage/songs/classic/Tchaikovsky-Waltz_Of_The_Flowers.mid new file mode 100644 index 00000000..a9e5fd6d Binary files /dev/null and b/storage/songs/classic/Tchaikovsky-Waltz_Of_The_Flowers.mid differ diff --git a/storage/songs/classic/Vaughan_Williams-Fantasia_On_Greensleeves.mid b/storage/songs/classic/Vaughan_Williams-Fantasia_On_Greensleeves.mid new file mode 100644 index 00000000..b4ad35bd Binary files /dev/null and b/storage/songs/classic/Vaughan_Williams-Fantasia_On_Greensleeves.mid differ diff --git a/storage/songs/classic/Vittorio_Monti-Czardas.mid b/storage/songs/classic/Vittorio_Monti-Czardas.mid new file mode 100644 index 00000000..821d4dfe Binary files /dev/null and b/storage/songs/classic/Vittorio_Monti-Czardas.mid differ diff --git a/storage/songs/classic/Vivaldi-Four_Seasons_-_Spring.mid b/storage/songs/classic/Vivaldi-Four_Seasons_-_Spring.mid new file mode 100644 index 00000000..3f543850 Binary files /dev/null and b/storage/songs/classic/Vivaldi-Four_Seasons_-_Spring.mid differ diff --git a/storage/songs/classic/Vivaldi-Four_Seasons_-_Summer.mid b/storage/songs/classic/Vivaldi-Four_Seasons_-_Summer.mid new file mode 100644 index 00000000..201deadc Binary files /dev/null and b/storage/songs/classic/Vivaldi-Four_Seasons_-_Summer.mid differ diff --git a/storage/songs/classic/Vivaldi-Four_Seasons_-_Winter.mid b/storage/songs/classic/Vivaldi-Four_Seasons_-_Winter.mid new file mode 100644 index 00000000..1ae45544 Binary files /dev/null and b/storage/songs/classic/Vivaldi-Four_Seasons_-_Winter.mid differ diff --git a/storage/songs/folk/Chinese_Folk-Jasmine_Flower.mid b/storage/songs/folk/Chinese_Folk-Jasmine_Flower.mid new file mode 100644 index 00000000..92e40f41 Binary files /dev/null and b/storage/songs/folk/Chinese_Folk-Jasmine_Flower.mid differ diff --git a/storage/songs/folk/English_Folk-Greensleeves.mid b/storage/songs/folk/English_Folk-Greensleeves.mid new file mode 100644 index 00000000..4b720b5a Binary files /dev/null and b/storage/songs/folk/English_Folk-Greensleeves.mid differ diff --git a/storage/songs/folk/English_Folk-Twinkle_Twinkle_Little_Star.mid b/storage/songs/folk/English_Folk-Twinkle_Twinkle_Little_Star.mid new file mode 100644 index 00000000..46c60b2a Binary files /dev/null and b/storage/songs/folk/English_Folk-Twinkle_Twinkle_Little_Star.mid differ diff --git a/storage/songs/folk/English_Folk-We_Wish_You_A_Marry_Christmas.mid b/storage/songs/folk/English_Folk-We_Wish_You_A_Marry_Christmas.mid new file mode 100644 index 00000000..3d1ec758 Binary files /dev/null and b/storage/songs/folk/English_Folk-We_Wish_You_A_Marry_Christmas.mid differ diff --git a/storage/songs/folk/English_Folk-We_Wish_You_A_Merry_Christmas.mid b/storage/songs/folk/English_Folk-We_Wish_You_A_Merry_Christmas.mid new file mode 100644 index 00000000..6828f2ab Binary files /dev/null and b/storage/songs/folk/English_Folk-We_Wish_You_A_Merry_Christmas.mid differ diff --git a/storage/songs/folk/Italian_Folk-Bella_Ciao.mid b/storage/songs/folk/Italian_Folk-Bella_Ciao.mid new file mode 100644 index 00000000..2579341b Binary files /dev/null and b/storage/songs/folk/Italian_Folk-Bella_Ciao.mid differ diff --git a/storage/songs/folk/Japanese_Folk-Sakura_Sakura.mid b/storage/songs/folk/Japanese_Folk-Sakura_Sakura.mid new file mode 100644 index 00000000..a4db89d1 Binary files /dev/null and b/storage/songs/folk/Japanese_Folk-Sakura_Sakura.mid differ diff --git a/storage/songs/folk/Korean_Folk-Arirang.mid b/storage/songs/folk/Korean_Folk-Arirang.mid new file mode 100644 index 00000000..cae56439 Binary files /dev/null and b/storage/songs/folk/Korean_Folk-Arirang.mid differ diff --git a/storage/songs/folk/Russian_Folk-Kalinka.mid b/storage/songs/folk/Russian_Folk-Kalinka.mid new file mode 100644 index 00000000..eacd5463 Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Kalinka.mid differ diff --git a/storage/songs/folk/Russian_Folk-Katyusha.mid b/storage/songs/folk/Russian_Folk-Katyusha.mid new file mode 100644 index 00000000..0e5a6367 Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Katyusha.mid differ diff --git a/storage/songs/folk/Russian_Folk-Korobeiniki.mid b/storage/songs/folk/Russian_Folk-Korobeiniki.mid new file mode 100644 index 00000000..c3c9fc6b Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Korobeiniki.mid differ diff --git a/storage/songs/folk/Russian_Folk-Oy,_Da_Ne_Vecher.mid b/storage/songs/folk/Russian_Folk-Oy,_Da_Ne_Vecher.mid new file mode 100644 index 00000000..fbb1cf52 Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Oy,_Da_Ne_Vecher.mid differ diff --git a/storage/songs/folk/Russian_Folk-Polyushka_Polye.mid b/storage/songs/folk/Russian_Folk-Polyushka_Polye.mid new file mode 100644 index 00000000..91e3dbe0 Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Polyushka_Polye.mid differ diff --git a/storage/songs/folk/Russian_Folk-Smuglyanka.mid b/storage/songs/folk/Russian_Folk-Smuglyanka.mid new file mode 100644 index 00000000..fa6f6a3e Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Smuglyanka.mid differ diff --git a/storage/songs/folk/Russian_Folk-Troika.mid b/storage/songs/folk/Russian_Folk-Troika.mid new file mode 100644 index 00000000..4b4aee0b Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Troika.mid differ diff --git a/storage/songs/folk/Russian_Folk-Tumbalalaika.mid b/storage/songs/folk/Russian_Folk-Tumbalalaika.mid new file mode 100644 index 00000000..3de69bae Binary files /dev/null and b/storage/songs/folk/Russian_Folk-Tumbalalaika.mid differ diff --git a/storage/songs/folk/Vietnamese_Folk-Bac_Kim_Thang.mid b/storage/songs/folk/Vietnamese_Folk-Bac_Kim_Thang.mid new file mode 100644 index 00000000..9a5cd5db Binary files /dev/null and b/storage/songs/folk/Vietnamese_Folk-Bac_Kim_Thang.mid differ diff --git a/storage/songs/folk/Vietnamese_Folk-Beo_Dat_May_Troi.mid b/storage/songs/folk/Vietnamese_Folk-Beo_Dat_May_Troi.mid new file mode 100644 index 00000000..77aef444 Binary files /dev/null and b/storage/songs/folk/Vietnamese_Folk-Beo_Dat_May_Troi.mid differ diff --git a/storage/songs/folk/Vietnamese_Folk-Cay_Truc_Xinh.mid b/storage/songs/folk/Vietnamese_Folk-Cay_Truc_Xinh.mid new file mode 100644 index 00000000..621c6055 Binary files /dev/null and b/storage/songs/folk/Vietnamese_Folk-Cay_Truc_Xinh.mid differ diff --git a/storage/songs/folk/Vietnamese_Folk-Co_La.mid b/storage/songs/folk/Vietnamese_Folk-Co_La.mid new file mode 100644 index 00000000..943503a6 Binary files /dev/null and b/storage/songs/folk/Vietnamese_Folk-Co_La.mid differ diff --git a/storage/songs/folk/Vietnamese_Folk-Ly_Cay_Bong.mid b/storage/songs/folk/Vietnamese_Folk-Ly_Cay_Bong.mid new file mode 100644 index 00000000..a5f3b2ff Binary files /dev/null and b/storage/songs/folk/Vietnamese_Folk-Ly_Cay_Bong.mid differ diff --git a/storage/songs/folk/Vietnamese_Folk-Nguoi_O_Dung_Ve.mid b/storage/songs/folk/Vietnamese_Folk-Nguoi_O_Dung_Ve.mid new file mode 100644 index 00000000..971b1612 Binary files /dev/null and b/storage/songs/folk/Vietnamese_Folk-Nguoi_O_Dung_Ve.mid differ diff --git a/storage/songs/folk/Vietnamese_Folk-Trong_Com.mid b/storage/songs/folk/Vietnamese_Folk-Trong_Com.mid new file mode 100644 index 00000000..81af35bd Binary files /dev/null and b/storage/songs/folk/Vietnamese_Folk-Trong_Com.mid differ diff --git a/storage/songs/kpop/BTS-Black_Swan.mid b/storage/songs/kpop/BTS-Black_Swan.mid new file mode 100644 index 00000000..1e805f2a Binary files /dev/null and b/storage/songs/kpop/BTS-Black_Swan.mid differ diff --git a/storage/songs/kpop/BTS-DNA.mid b/storage/songs/kpop/BTS-DNA.mid new file mode 100644 index 00000000..7cbe8330 Binary files /dev/null and b/storage/songs/kpop/BTS-DNA.mid differ diff --git a/storage/songs/kpop/BTS-Dynamite.mid b/storage/songs/kpop/BTS-Dynamite.mid new file mode 100644 index 00000000..faf813ef Binary files /dev/null and b/storage/songs/kpop/BTS-Dynamite.mid differ diff --git a/storage/songs/kpop/BTS-I_Need_U.mid b/storage/songs/kpop/BTS-I_Need_U.mid new file mode 100644 index 00000000..5896ddb1 Binary files /dev/null and b/storage/songs/kpop/BTS-I_Need_U.mid differ diff --git a/storage/songs/kpop/BTS-Life_Goes_On.mid b/storage/songs/kpop/BTS-Life_Goes_On.mid new file mode 100644 index 00000000..c882eaad Binary files /dev/null and b/storage/songs/kpop/BTS-Life_Goes_On.mid differ diff --git a/storage/songs/kpop/Blackpink-How_You_Like_That.mid b/storage/songs/kpop/Blackpink-How_You_Like_That.mid new file mode 100644 index 00000000..316347c1 Binary files /dev/null and b/storage/songs/kpop/Blackpink-How_You_Like_That.mid differ diff --git a/storage/songs/kpop/Blackpink-Kill_This_Love_Piano.mid b/storage/songs/kpop/Blackpink-Kill_This_Love_Piano.mid new file mode 100644 index 00000000..b3a16aeb Binary files /dev/null and b/storage/songs/kpop/Blackpink-Kill_This_Love_Piano.mid differ diff --git a/storage/songs/kpop/Blackpink-You_Never_Know.mid b/storage/songs/kpop/Blackpink-You_Never_Know.mid new file mode 100644 index 00000000..b62e4e94 Binary files /dev/null and b/storage/songs/kpop/Blackpink-You_Never_Know.mid differ diff --git a/storage/songs/kpop/SHINee-Hello.mid b/storage/songs/kpop/SHINee-Hello.mid new file mode 100644 index 00000000..413fad69 Binary files /dev/null and b/storage/songs/kpop/SHINee-Hello.mid differ diff --git a/storage/songs/other/Billie_Eilish,_Khalid-Lovely.mid b/storage/songs/other/Billie_Eilish,_Khalid-Lovely.mid new file mode 100644 index 00000000..71a1007f Binary files /dev/null and b/storage/songs/other/Billie_Eilish,_Khalid-Lovely.mid differ diff --git a/storage/songs/other/Brad_Breeck-Gravity_Falls,_Opening_Theme.mid b/storage/songs/other/Brad_Breeck-Gravity_Falls,_Opening_Theme.mid new file mode 100644 index 00000000..73717c7f Binary files /dev/null and b/storage/songs/other/Brad_Breeck-Gravity_Falls,_Opening_Theme.mid differ diff --git a/storage/songs/other/Erik_Satie-Gnossienne.mid b/storage/songs/other/Erik_Satie-Gnossienne.mid new file mode 100644 index 00000000..63d9a7dc Binary files /dev/null and b/storage/songs/other/Erik_Satie-Gnossienne.mid differ diff --git a/storage/songs/other/Erik_Satie-Gymnopedie.mid b/storage/songs/other/Erik_Satie-Gymnopedie.mid new file mode 100644 index 00000000..67fd4215 Binary files /dev/null and b/storage/songs/other/Erik_Satie-Gymnopedie.mid differ diff --git a/storage/songs/other/Eva_Cassidy-Autumn_Leaves.mid b/storage/songs/other/Eva_Cassidy-Autumn_Leaves.mid new file mode 100644 index 00000000..c431b27d Binary files /dev/null and b/storage/songs/other/Eva_Cassidy-Autumn_Leaves.mid differ diff --git a/storage/songs/other/Frank_Sinatra-Fly_Me_To_The_Moon.mid b/storage/songs/other/Frank_Sinatra-Fly_Me_To_The_Moon.mid new file mode 100644 index 00000000..81155822 Binary files /dev/null and b/storage/songs/other/Frank_Sinatra-Fly_Me_To_The_Moon.mid differ diff --git a/storage/songs/other/Franz_Xaver_Gruber-Silent_Night.mid b/storage/songs/other/Franz_Xaver_Gruber-Silent_Night.mid new file mode 100644 index 00000000..57ad8d58 Binary files /dev/null and b/storage/songs/other/Franz_Xaver_Gruber-Silent_Night.mid differ diff --git a/storage/songs/other/Gary_Jules-Mad_World.mid b/storage/songs/other/Gary_Jules-Mad_World.mid new file mode 100644 index 00000000..e53a39a3 Binary files /dev/null and b/storage/songs/other/Gary_Jules-Mad_World.mid differ diff --git a/storage/songs/other/Handel_Halvorsen-Passacaglia.mid b/storage/songs/other/Handel_Halvorsen-Passacaglia.mid new file mode 100644 index 00000000..2f7918dc Binary files /dev/null and b/storage/songs/other/Handel_Halvorsen-Passacaglia.mid differ diff --git a/storage/songs/other/Hans_Zimmer-Interstellar,_Main_Theme.mid b/storage/songs/other/Hans_Zimmer-Interstellar,_Main_Theme.mid new file mode 100644 index 00000000..eb52f892 Binary files /dev/null and b/storage/songs/other/Hans_Zimmer-Interstellar,_Main_Theme.mid differ diff --git a/storage/songs/other/Joe_Hisaishi-Innocent.mid b/storage/songs/other/Joe_Hisaishi-Innocent.mid new file mode 100644 index 00000000..a52a4062 Binary files /dev/null and b/storage/songs/other/Joe_Hisaishi-Innocent.mid differ diff --git a/storage/songs/other/Joe_Hisaishi-Merry_Go_Round_of_Life,_Howls_Moving_Castle.mid b/storage/songs/other/Joe_Hisaishi-Merry_Go_Round_of_Life,_Howls_Moving_Castle.mid new file mode 100644 index 00000000..ad8a2026 Binary files /dev/null and b/storage/songs/other/Joe_Hisaishi-Merry_Go_Round_of_Life,_Howls_Moving_Castle.mid differ diff --git a/storage/songs/other/Joe_Hisaishi-One_Summers_Day,_Spirited_Away.mid b/storage/songs/other/Joe_Hisaishi-One_Summers_Day,_Spirited_Away.mid new file mode 100644 index 00000000..dd018fb3 Binary files /dev/null and b/storage/songs/other/Joe_Hisaishi-One_Summers_Day,_Spirited_Away.mid differ diff --git a/storage/songs/other/Joe_Hisaishi-Summer.mid b/storage/songs/other/Joe_Hisaishi-Summer.mid new file mode 100644 index 00000000..fbca3dd5 Binary files /dev/null and b/storage/songs/other/Joe_Hisaishi-Summer.mid differ diff --git a/storage/songs/other/Jose_Feliciano-Feliz_Navidad.mid b/storage/songs/other/Jose_Feliciano-Feliz_Navidad.mid new file mode 100644 index 00000000..808cf49b Binary files /dev/null and b/storage/songs/other/Jose_Feliciano-Feliz_Navidad.mid differ diff --git a/storage/songs/other/Kazumi_Totaka-Mii_Channel.mid b/storage/songs/other/Kazumi_Totaka-Mii_Channel.mid new file mode 100644 index 00000000..16b9d519 Binary files /dev/null and b/storage/songs/other/Kazumi_Totaka-Mii_Channel.mid differ diff --git a/storage/songs/other/Kimi_No_Na_Wa-Sparkle.mid b/storage/songs/other/Kimi_No_Na_Wa-Sparkle.mid new file mode 100644 index 00000000..2c8c3c65 Binary files /dev/null and b/storage/songs/other/Kimi_No_Na_Wa-Sparkle.mid differ diff --git a/storage/songs/other/Leonard_Cohen-Hallelujah.mid b/storage/songs/other/Leonard_Cohen-Hallelujah.mid new file mode 100644 index 00000000..96182467 Binary files /dev/null and b/storage/songs/other/Leonard_Cohen-Hallelujah.mid differ diff --git a/storage/songs/other/Lewis_Capaldi-Someone_You_Loved.mid b/storage/songs/other/Lewis_Capaldi-Someone_You_Loved.mid new file mode 100644 index 00000000..1a65f708 Binary files /dev/null and b/storage/songs/other/Lewis_Capaldi-Someone_You_Loved.mid differ diff --git a/storage/songs/other/Ludovico_Einaudi-Experience.mid b/storage/songs/other/Ludovico_Einaudi-Experience.mid new file mode 100644 index 00000000..4949bc00 Binary files /dev/null and b/storage/songs/other/Ludovico_Einaudi-Experience.mid differ diff --git a/storage/songs/other/Ludovico_Einaudi-Una_Mattina.mid b/storage/songs/other/Ludovico_Einaudi-Una_Mattina.mid new file mode 100644 index 00000000..9fd6844f Binary files /dev/null and b/storage/songs/other/Ludovico_Einaudi-Una_Mattina.mid differ diff --git a/storage/songs/other/Ludovico_Einoudi-Nuvole_Bianche.mid b/storage/songs/other/Ludovico_Einoudi-Nuvole_Bianche.mid new file mode 100644 index 00000000..11344057 Binary files /dev/null and b/storage/songs/other/Ludovico_Einoudi-Nuvole_Bianche.mid differ diff --git a/storage/songs/other/Megalovania-Undertale.mid b/storage/songs/other/Megalovania-Undertale.mid new file mode 100644 index 00000000..047c79c4 Binary files /dev/null and b/storage/songs/other/Megalovania-Undertale.mid differ diff --git a/storage/songs/other/Meredith_Willson-It's_Beginning_To_Look_A_Lot_Like_Christmas.mid b/storage/songs/other/Meredith_Willson-It's_Beginning_To_Look_A_Lot_Like_Christmas.mid new file mode 100644 index 00000000..b3c3885e Binary files /dev/null and b/storage/songs/other/Meredith_Willson-It's_Beginning_To_Look_A_Lot_Like_Christmas.mid differ diff --git a/storage/songs/other/Michael_Buble-White_Christmas.mid b/storage/songs/other/Michael_Buble-White_Christmas.mid new file mode 100644 index 00000000..539848bf Binary files /dev/null and b/storage/songs/other/Michael_Buble-White_Christmas.mid differ diff --git a/storage/songs/other/Mr_Grinch-You're_A_Mean_One.mid b/storage/songs/other/Mr_Grinch-You're_A_Mean_One.mid new file mode 100644 index 00000000..12f3eddb Binary files /dev/null and b/storage/songs/other/Mr_Grinch-You're_A_Mean_One.mid differ diff --git a/storage/songs/other/Mykola_Leontovych-Carol_Of_The_Bells.mid b/storage/songs/other/Mykola_Leontovych-Carol_Of_The_Bells.mid new file mode 100644 index 00000000..16276a5d Binary files /dev/null and b/storage/songs/other/Mykola_Leontovych-Carol_Of_The_Bells.mid differ diff --git a/storage/songs/other/Pachelbel-Canon_In_D.mid b/storage/songs/other/Pachelbel-Canon_In_D.mid new file mode 100644 index 00000000..29b2eb04 Binary files /dev/null and b/storage/songs/other/Pachelbel-Canon_In_D.mid differ diff --git a/storage/songs/other/Paul_De_Senneville-Mariage_d'Amour.mid b/storage/songs/other/Paul_De_Senneville-Mariage_d'Amour.mid new file mode 100644 index 00000000..60397390 Binary files /dev/null and b/storage/songs/other/Paul_De_Senneville-Mariage_d'Amour.mid differ diff --git a/storage/songs/other/Ramin_Djawadi-Game_Of_Thrones.mid b/storage/songs/other/Ramin_Djawadi-Game_Of_Thrones.mid new file mode 100644 index 00000000..56993000 Binary files /dev/null and b/storage/songs/other/Ramin_Djawadi-Game_Of_Thrones.mid differ diff --git a/storage/songs/other/Richard_Clayderman-Ballade_Pour_Adeline.mid b/storage/songs/other/Richard_Clayderman-Ballade_Pour_Adeline.mid new file mode 100644 index 00000000..58e9f549 Binary files /dev/null and b/storage/songs/other/Richard_Clayderman-Ballade_Pour_Adeline.mid differ diff --git a/storage/songs/other/Rick_Astley-Never_Gonna_Give_You_Up.mid b/storage/songs/other/Rick_Astley-Never_Gonna_Give_You_Up.mid new file mode 100644 index 00000000..92c9433e Binary files /dev/null and b/storage/songs/other/Rick_Astley-Never_Gonna_Give_You_Up.mid differ diff --git a/storage/songs/other/Scott_Joplin-Maple_Leaf_Rag.mid b/storage/songs/other/Scott_Joplin-Maple_Leaf_Rag.mid new file mode 100644 index 00000000..63f2a88a Binary files /dev/null and b/storage/songs/other/Scott_Joplin-Maple_Leaf_Rag.mid differ diff --git a/storage/songs/other/Shostakovich-Waltz_No._2.mid b/storage/songs/other/Shostakovich-Waltz_No._2.mid new file mode 100644 index 00000000..b72fdaeb Binary files /dev/null and b/storage/songs/other/Shostakovich-Waltz_No._2.mid differ diff --git a/storage/songs/other/Takahiro_Obata-Isabella's_Lullaby.mid b/storage/songs/other/Takahiro_Obata-Isabella's_Lullaby.mid new file mode 100644 index 00000000..4f400eca Binary files /dev/null and b/storage/songs/other/Takahiro_Obata-Isabella's_Lullaby.mid differ diff --git a/storage/songs/other/The_Scientist-Coldplay.mid b/storage/songs/other/The_Scientist-Coldplay.mid new file mode 100644 index 00000000..bab592ca Binary files /dev/null and b/storage/songs/other/The_Scientist-Coldplay.mid differ diff --git a/storage/songs/other/Tokyo_Ghoul-Unravel.mid b/storage/songs/other/Tokyo_Ghoul-Unravel.mid new file mode 100644 index 00000000..e0c4828a Binary files /dev/null and b/storage/songs/other/Tokyo_Ghoul-Unravel.mid differ diff --git a/storage/songs/other/Vince_Guaraldi-Linus_And_Lucy.mid b/storage/songs/other/Vince_Guaraldi-Linus_And_Lucy.mid new file mode 100644 index 00000000..d1e66d1e Binary files /dev/null and b/storage/songs/other/Vince_Guaraldi-Linus_And_Lucy.mid differ diff --git a/storage/songs/other/Wham!-Last_Christmas-.mid b/storage/songs/other/Wham!-Last_Christmas-.mid new file mode 100644 index 00000000..b7d1ac08 Binary files /dev/null and b/storage/songs/other/Wham!-Last_Christmas-.mid differ diff --git a/storage/songs/other/Yann_Tiersen-Comptine_Dune_Autre_Ete.mid b/storage/songs/other/Yann_Tiersen-Comptine_Dune_Autre_Ete.mid new file mode 100644 index 00000000..e8fa45a7 Binary files /dev/null and b/storage/songs/other/Yann_Tiersen-Comptine_Dune_Autre_Ete.mid differ diff --git a/storage/songs/other/Yann_Tiersen-La_Valse_dAmelie.mid b/storage/songs/other/Yann_Tiersen-La_Valse_dAmelie.mid new file mode 100644 index 00000000..18a52a0a Binary files /dev/null and b/storage/songs/other/Yann_Tiersen-La_Valse_dAmelie.mid differ diff --git a/storage/songs/other/Yiruma-Kiss_The_Rain.mid b/storage/songs/other/Yiruma-Kiss_The_Rain.mid new file mode 100644 index 00000000..18fbcf3e Binary files /dev/null and b/storage/songs/other/Yiruma-Kiss_The_Rain.mid differ diff --git a/storage/songs/other/Yiruma-River_Flows_In_You.mid b/storage/songs/other/Yiruma-River_Flows_In_You.mid new file mode 100644 index 00000000..8e9b321f Binary files /dev/null and b/storage/songs/other/Yiruma-River_Flows_In_You.mid differ diff --git a/storage/songs/pop/Alan_Walker-Alone.mid b/storage/songs/pop/Alan_Walker-Alone.mid new file mode 100644 index 00000000..30d099c5 Binary files /dev/null and b/storage/songs/pop/Alan_Walker-Alone.mid differ diff --git a/storage/songs/pop/Alan_Walker-Faded.mid b/storage/songs/pop/Alan_Walker-Faded.mid new file mode 100644 index 00000000..9f2bd3d8 Binary files /dev/null and b/storage/songs/pop/Alan_Walker-Faded.mid differ diff --git a/storage/songs/pop/Alan_Walker-Spectre.mid b/storage/songs/pop/Alan_Walker-Spectre.mid new file mode 100644 index 00000000..1c6a2885 Binary files /dev/null and b/storage/songs/pop/Alan_Walker-Spectre.mid differ diff --git a/storage/songs/pop/Bruno_Mars-When_I_Was_Your_Man.mid b/storage/songs/pop/Bruno_Mars-When_I_Was_Your_Man.mid new file mode 100644 index 00000000..3614c956 Binary files /dev/null and b/storage/songs/pop/Bruno_Mars-When_I_Was_Your_Man.mid differ diff --git a/storage/songs/pop/Celine_Dion-My_Heart_Will_Go_On.mid b/storage/songs/pop/Celine_Dion-My_Heart_Will_Go_On.mid new file mode 100644 index 00000000..2ec211ce Binary files /dev/null and b/storage/songs/pop/Celine_Dion-My_Heart_Will_Go_On.mid differ diff --git a/storage/songs/pop/Charlie_Puth-See_You_Again.mid b/storage/songs/pop/Charlie_Puth-See_You_Again.mid new file mode 100644 index 00000000..bbc6b2d1 Binary files /dev/null and b/storage/songs/pop/Charlie_Puth-See_You_Again.mid differ diff --git a/storage/songs/pop/Christina_Perri-A_Thousand_Years.mid b/storage/songs/pop/Christina_Perri-A_Thousand_Years.mid new file mode 100644 index 00000000..8f128739 Binary files /dev/null and b/storage/songs/pop/Christina_Perri-A_Thousand_Years.mid differ diff --git a/storage/songs/pop/Deaf_Kev-Invincible.mid b/storage/songs/pop/Deaf_Kev-Invincible.mid new file mode 100644 index 00000000..fa5c374e Binary files /dev/null and b/storage/songs/pop/Deaf_Kev-Invincible.mid differ diff --git a/storage/songs/pop/Different_Heaven,_EHDE-My_Heart.mid b/storage/songs/pop/Different_Heaven,_EHDE-My_Heart.mid new file mode 100644 index 00000000..83a56d30 Binary files /dev/null and b/storage/songs/pop/Different_Heaven,_EHDE-My_Heart.mid differ diff --git a/storage/songs/pop/Disfigure-Blank.mid b/storage/songs/pop/Disfigure-Blank.mid new file mode 100644 index 00000000..0fbd7d2d Binary files /dev/null and b/storage/songs/pop/Disfigure-Blank.mid differ diff --git a/storage/songs/pop/Disney_Pixar's_Up-Married_Life.mid b/storage/songs/pop/Disney_Pixar's_Up-Married_Life.mid new file mode 100644 index 00000000..03508376 Binary files /dev/null and b/storage/songs/pop/Disney_Pixar's_Up-Married_Life.mid differ diff --git a/storage/songs/pop/Ed_Sheeran-I_See_Fire.mid b/storage/songs/pop/Ed_Sheeran-I_See_Fire.mid new file mode 100644 index 00000000..e4b61fbe Binary files /dev/null and b/storage/songs/pop/Ed_Sheeran-I_See_Fire.mid differ diff --git a/storage/songs/pop/Ed_Sheeran-Perfect.mid b/storage/songs/pop/Ed_Sheeran-Perfect.mid new file mode 100644 index 00000000..cbf1e49b Binary files /dev/null and b/storage/songs/pop/Ed_Sheeran-Perfect.mid differ diff --git a/storage/songs/pop/Ed_Sheeran-Photograph.mid b/storage/songs/pop/Ed_Sheeran-Photograph.mid new file mode 100644 index 00000000..3524d0e9 Binary files /dev/null and b/storage/songs/pop/Ed_Sheeran-Photograph.mid differ diff --git a/storage/songs/pop/Ed_Sheeran-Shape_Of_You.mid b/storage/songs/pop/Ed_Sheeran-Shape_Of_You.mid new file mode 100644 index 00000000..ac042b5e Binary files /dev/null and b/storage/songs/pop/Ed_Sheeran-Shape_Of_You.mid differ diff --git a/storage/songs/pop/Ed_Sheeran-Thinking_Out_Loud.mid b/storage/songs/pop/Ed_Sheeran-Thinking_Out_Loud.mid new file mode 100644 index 00000000..d59f2e0a Binary files /dev/null and b/storage/songs/pop/Ed_Sheeran-Thinking_Out_Loud.mid differ diff --git a/storage/songs/pop/Elektronomia-Sky_High.mid b/storage/songs/pop/Elektronomia-Sky_High.mid new file mode 100644 index 00000000..e591d951 Binary files /dev/null and b/storage/songs/pop/Elektronomia-Sky_High.mid differ diff --git a/storage/songs/pop/Imagine_Dragons-Believer.mid b/storage/songs/pop/Imagine_Dragons-Believer.mid new file mode 100644 index 00000000..7f2d3988 Binary files /dev/null and b/storage/songs/pop/Imagine_Dragons-Believer.mid differ diff --git a/storage/songs/pop/Janji-Heroes_Tonight.mid b/storage/songs/pop/Janji-Heroes_Tonight.mid new file mode 100644 index 00000000..f029684d Binary files /dev/null and b/storage/songs/pop/Janji-Heroes_Tonight.mid differ diff --git a/storage/songs/pop/John_Legend-All_Of_Me.mid b/storage/songs/pop/John_Legend-All_Of_Me.mid new file mode 100644 index 00000000..b0a90438 Binary files /dev/null and b/storage/songs/pop/John_Legend-All_Of_Me.mid differ diff --git a/storage/songs/pop/John_Lennon-Imagine.mid b/storage/songs/pop/John_Lennon-Imagine.mid new file mode 100644 index 00000000..9a40a43f Binary files /dev/null and b/storage/songs/pop/John_Lennon-Imagine.mid differ diff --git a/storage/songs/pop/Luis_Fonsi_ft_Daddy_Yankee-Despacito.mid b/storage/songs/pop/Luis_Fonsi_ft_Daddy_Yankee-Despacito.mid new file mode 100644 index 00000000..1c642b5c Binary files /dev/null and b/storage/songs/pop/Luis_Fonsi_ft_Daddy_Yankee-Despacito.mid differ diff --git a/storage/songs/pop/Mariah_Carey-All_I_Want_For_Christmas_Is_You.mid b/storage/songs/pop/Mariah_Carey-All_I_Want_For_Christmas_Is_You.mid new file mode 100644 index 00000000..96a58c19 Binary files /dev/null and b/storage/songs/pop/Mariah_Carey-All_I_Want_For_Christmas_Is_You.mid differ diff --git a/storage/songs/pop/One_Republic-Counting_Stars.mid b/storage/songs/pop/One_Republic-Counting_Stars.mid new file mode 100644 index 00000000..1deedeca Binary files /dev/null and b/storage/songs/pop/One_Republic-Counting_Stars.mid differ diff --git a/storage/songs/pop/Shakira-Waka_Waka_(This_Time_For_Africa).mid b/storage/songs/pop/Shakira-Waka_Waka_(This_Time_For_Africa).mid new file mode 100644 index 00000000..f9984971 Binary files /dev/null and b/storage/songs/pop/Shakira-Waka_Waka_(This_Time_For_Africa).mid differ diff --git a/storage/sounds/acoustic-guitar/31.mp3 b/storage/sounds/acoustic-guitar/31.mp3 new file mode 100644 index 00000000..e981b11f Binary files /dev/null and b/storage/sounds/acoustic-guitar/31.mp3 differ diff --git a/storage/sounds/acoustic-guitar/33.mp3 b/storage/sounds/acoustic-guitar/33.mp3 new file mode 100644 index 00000000..51b52ed5 Binary files /dev/null and b/storage/sounds/acoustic-guitar/33.mp3 differ diff --git a/storage/sounds/acoustic-guitar/36.mp3 b/storage/sounds/acoustic-guitar/36.mp3 new file mode 100644 index 00000000..f8edb73b Binary files /dev/null and b/storage/sounds/acoustic-guitar/36.mp3 differ diff --git a/storage/sounds/acoustic-guitar/39.mp3 b/storage/sounds/acoustic-guitar/39.mp3 new file mode 100644 index 00000000..9d84c868 Binary files /dev/null and b/storage/sounds/acoustic-guitar/39.mp3 differ diff --git a/storage/sounds/acoustic-guitar/43.mp3 b/storage/sounds/acoustic-guitar/43.mp3 new file mode 100644 index 00000000..a43a0b8c Binary files /dev/null and b/storage/sounds/acoustic-guitar/43.mp3 differ diff --git a/storage/sounds/acoustic-guitar/45.mp3 b/storage/sounds/acoustic-guitar/45.mp3 new file mode 100644 index 00000000..61ab390a Binary files /dev/null and b/storage/sounds/acoustic-guitar/45.mp3 differ diff --git a/storage/sounds/acoustic-guitar/48.mp3 b/storage/sounds/acoustic-guitar/48.mp3 new file mode 100644 index 00000000..b9d8faa3 Binary files /dev/null and b/storage/sounds/acoustic-guitar/48.mp3 differ diff --git a/storage/sounds/acoustic-guitar/52.mp3 b/storage/sounds/acoustic-guitar/52.mp3 new file mode 100644 index 00000000..a4f226ac Binary files /dev/null and b/storage/sounds/acoustic-guitar/52.mp3 differ diff --git a/storage/sounds/acoustic-guitar/54.mp3 b/storage/sounds/acoustic-guitar/54.mp3 new file mode 100644 index 00000000..e9bed2f9 Binary files /dev/null and b/storage/sounds/acoustic-guitar/54.mp3 differ diff --git a/storage/sounds/acoustic-guitar/57.mp3 b/storage/sounds/acoustic-guitar/57.mp3 new file mode 100644 index 00000000..c200b9b0 Binary files /dev/null and b/storage/sounds/acoustic-guitar/57.mp3 differ diff --git a/storage/sounds/acoustic-guitar/60.mp3 b/storage/sounds/acoustic-guitar/60.mp3 new file mode 100644 index 00000000..3640f00a Binary files /dev/null and b/storage/sounds/acoustic-guitar/60.mp3 differ diff --git a/storage/sounds/acoustic-guitar/63.mp3 b/storage/sounds/acoustic-guitar/63.mp3 new file mode 100644 index 00000000..74b7b3c3 Binary files /dev/null and b/storage/sounds/acoustic-guitar/63.mp3 differ diff --git a/storage/sounds/acoustic-guitar/66.mp3 b/storage/sounds/acoustic-guitar/66.mp3 new file mode 100644 index 00000000..432b4cd0 Binary files /dev/null and b/storage/sounds/acoustic-guitar/66.mp3 differ diff --git a/storage/sounds/acoustic-guitar/69.mp3 b/storage/sounds/acoustic-guitar/69.mp3 new file mode 100644 index 00000000..dd81fc7f Binary files /dev/null and b/storage/sounds/acoustic-guitar/69.mp3 differ diff --git a/storage/sounds/acoustic-guitar/72.mp3 b/storage/sounds/acoustic-guitar/72.mp3 new file mode 100644 index 00000000..bbaa5a33 Binary files /dev/null and b/storage/sounds/acoustic-guitar/72.mp3 differ diff --git a/storage/sounds/acoustic-guitar/75.mp3 b/storage/sounds/acoustic-guitar/75.mp3 new file mode 100644 index 00000000..58c0e3f3 Binary files /dev/null and b/storage/sounds/acoustic-guitar/75.mp3 differ diff --git a/storage/sounds/acoustic-guitar/78.mp3 b/storage/sounds/acoustic-guitar/78.mp3 new file mode 100644 index 00000000..553fd369 Binary files /dev/null and b/storage/sounds/acoustic-guitar/78.mp3 differ diff --git a/storage/sounds/acoustic-guitar/81.mp3 b/storage/sounds/acoustic-guitar/81.mp3 new file mode 100644 index 00000000..7570a606 Binary files /dev/null and b/storage/sounds/acoustic-guitar/81.mp3 differ diff --git a/storage/sounds/acoustic-guitar/84.mp3 b/storage/sounds/acoustic-guitar/84.mp3 new file mode 100644 index 00000000..8608eb42 Binary files /dev/null and b/storage/sounds/acoustic-guitar/84.mp3 differ diff --git a/storage/sounds/electric-guitar/40.mp3 b/storage/sounds/electric-guitar/40.mp3 new file mode 100644 index 00000000..c54a0aed Binary files /dev/null and b/storage/sounds/electric-guitar/40.mp3 differ diff --git a/storage/sounds/electric-guitar/44.mp3 b/storage/sounds/electric-guitar/44.mp3 new file mode 100644 index 00000000..38bc39b8 Binary files /dev/null and b/storage/sounds/electric-guitar/44.mp3 differ diff --git a/storage/sounds/electric-guitar/48.mp3 b/storage/sounds/electric-guitar/48.mp3 new file mode 100644 index 00000000..dfe8c195 Binary files /dev/null and b/storage/sounds/electric-guitar/48.mp3 differ diff --git a/storage/sounds/electric-guitar/52.mp3 b/storage/sounds/electric-guitar/52.mp3 new file mode 100644 index 00000000..f24194b8 Binary files /dev/null and b/storage/sounds/electric-guitar/52.mp3 differ diff --git a/storage/sounds/electric-guitar/56.mp3 b/storage/sounds/electric-guitar/56.mp3 new file mode 100644 index 00000000..e181fbab Binary files /dev/null and b/storage/sounds/electric-guitar/56.mp3 differ diff --git a/storage/sounds/electric-guitar/60.mp3 b/storage/sounds/electric-guitar/60.mp3 new file mode 100644 index 00000000..d128bb5c Binary files /dev/null and b/storage/sounds/electric-guitar/60.mp3 differ diff --git a/storage/sounds/electric-guitar/65.mp3 b/storage/sounds/electric-guitar/65.mp3 new file mode 100644 index 00000000..b7e7d339 Binary files /dev/null and b/storage/sounds/electric-guitar/65.mp3 differ diff --git a/storage/sounds/electric-guitar/68.mp3 b/storage/sounds/electric-guitar/68.mp3 new file mode 100644 index 00000000..ac800819 Binary files /dev/null and b/storage/sounds/electric-guitar/68.mp3 differ diff --git a/storage/sounds/electric-guitar/72.mp3 b/storage/sounds/electric-guitar/72.mp3 new file mode 100644 index 00000000..716b72d9 Binary files /dev/null and b/storage/sounds/electric-guitar/72.mp3 differ diff --git a/storage/sounds/electric-guitar/76.mp3 b/storage/sounds/electric-guitar/76.mp3 new file mode 100644 index 00000000..af994d08 Binary files /dev/null and b/storage/sounds/electric-guitar/76.mp3 differ diff --git a/storage/sounds/electric-guitar/80.mp3 b/storage/sounds/electric-guitar/80.mp3 new file mode 100644 index 00000000..1bf0e95c Binary files /dev/null and b/storage/sounds/electric-guitar/80.mp3 differ diff --git a/storage/sounds/electric-guitar/84.mp3 b/storage/sounds/electric-guitar/84.mp3 new file mode 100644 index 00000000..34c4a2d0 Binary files /dev/null and b/storage/sounds/electric-guitar/84.mp3 differ diff --git a/storage/sounds/piano/102.mp3 b/storage/sounds/piano/102.mp3 new file mode 100644 index 00000000..53d614eb Binary files /dev/null and b/storage/sounds/piano/102.mp3 differ diff --git a/storage/sounds/piano/105.mp3 b/storage/sounds/piano/105.mp3 new file mode 100644 index 00000000..590aa65b Binary files /dev/null and b/storage/sounds/piano/105.mp3 differ diff --git a/storage/sounds/piano/107.mp3 b/storage/sounds/piano/107.mp3 new file mode 100644 index 00000000..bb5f001d Binary files /dev/null and b/storage/sounds/piano/107.mp3 differ diff --git a/storage/sounds/piano/24.mp3 b/storage/sounds/piano/24.mp3 new file mode 100644 index 00000000..df47bebe Binary files /dev/null and b/storage/sounds/piano/24.mp3 differ diff --git a/storage/sounds/piano/30.mp3 b/storage/sounds/piano/30.mp3 new file mode 100644 index 00000000..8fca67e7 Binary files /dev/null and b/storage/sounds/piano/30.mp3 differ diff --git a/storage/sounds/piano/35.mp3 b/storage/sounds/piano/35.mp3 new file mode 100644 index 00000000..e6a47204 Binary files /dev/null and b/storage/sounds/piano/35.mp3 differ diff --git a/storage/sounds/piano/39.mp3 b/storage/sounds/piano/39.mp3 new file mode 100644 index 00000000..73b5ce8c Binary files /dev/null and b/storage/sounds/piano/39.mp3 differ diff --git a/storage/sounds/piano/42.mp3 b/storage/sounds/piano/42.mp3 new file mode 100644 index 00000000..54af2463 Binary files /dev/null and b/storage/sounds/piano/42.mp3 differ diff --git a/storage/sounds/piano/47.mp3 b/storage/sounds/piano/47.mp3 new file mode 100644 index 00000000..3bf46757 Binary files /dev/null and b/storage/sounds/piano/47.mp3 differ diff --git a/storage/sounds/piano/51.mp3 b/storage/sounds/piano/51.mp3 new file mode 100644 index 00000000..ac6ce25d Binary files /dev/null and b/storage/sounds/piano/51.mp3 differ diff --git a/storage/sounds/piano/54.mp3 b/storage/sounds/piano/54.mp3 new file mode 100644 index 00000000..2ebf56fb Binary files /dev/null and b/storage/sounds/piano/54.mp3 differ diff --git a/storage/sounds/piano/57.mp3 b/storage/sounds/piano/57.mp3 new file mode 100644 index 00000000..7550f38a Binary files /dev/null and b/storage/sounds/piano/57.mp3 differ diff --git a/storage/sounds/piano/60.mp3 b/storage/sounds/piano/60.mp3 new file mode 100644 index 00000000..840619d0 Binary files /dev/null and b/storage/sounds/piano/60.mp3 differ diff --git a/storage/sounds/piano/63.mp3 b/storage/sounds/piano/63.mp3 new file mode 100644 index 00000000..c673f1ee Binary files /dev/null and b/storage/sounds/piano/63.mp3 differ diff --git a/storage/sounds/piano/66.mp3 b/storage/sounds/piano/66.mp3 new file mode 100644 index 00000000..8f4d1b22 Binary files /dev/null and b/storage/sounds/piano/66.mp3 differ diff --git a/storage/sounds/piano/69.mp3 b/storage/sounds/piano/69.mp3 new file mode 100644 index 00000000..0721f6a4 Binary files /dev/null and b/storage/sounds/piano/69.mp3 differ diff --git a/storage/sounds/piano/72.mp3 b/storage/sounds/piano/72.mp3 new file mode 100644 index 00000000..39b50aaf Binary files /dev/null and b/storage/sounds/piano/72.mp3 differ diff --git a/storage/sounds/piano/75.mp3 b/storage/sounds/piano/75.mp3 new file mode 100644 index 00000000..9ad78689 Binary files /dev/null and b/storage/sounds/piano/75.mp3 differ diff --git a/storage/sounds/piano/78.mp3 b/storage/sounds/piano/78.mp3 new file mode 100644 index 00000000..ad2508f4 Binary files /dev/null and b/storage/sounds/piano/78.mp3 differ diff --git a/storage/sounds/piano/81.mp3 b/storage/sounds/piano/81.mp3 new file mode 100644 index 00000000..4d5f0bd6 Binary files /dev/null and b/storage/sounds/piano/81.mp3 differ diff --git a/storage/sounds/piano/84.mp3 b/storage/sounds/piano/84.mp3 new file mode 100644 index 00000000..996fe3f6 Binary files /dev/null and b/storage/sounds/piano/84.mp3 differ diff --git a/storage/sounds/piano/87.mp3 b/storage/sounds/piano/87.mp3 new file mode 100644 index 00000000..937594cb Binary files /dev/null and b/storage/sounds/piano/87.mp3 differ diff --git a/storage/sounds/piano/90.mp3 b/storage/sounds/piano/90.mp3 new file mode 100644 index 00000000..bdbfea9d Binary files /dev/null and b/storage/sounds/piano/90.mp3 differ diff --git a/storage/sounds/piano/93.mp3 b/storage/sounds/piano/93.mp3 new file mode 100644 index 00000000..061a2382 Binary files /dev/null and b/storage/sounds/piano/93.mp3 differ diff --git a/storage/sounds/piano/96.mp3 b/storage/sounds/piano/96.mp3 new file mode 100644 index 00000000..4646f744 Binary files /dev/null and b/storage/sounds/piano/96.mp3 differ diff --git a/storage/sounds/piano/99.mp3 b/storage/sounds/piano/99.mp3 new file mode 100644 index 00000000..c981f87b Binary files /dev/null and b/storage/sounds/piano/99.mp3 differ diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..22261132 --- /dev/null +++ b/web/index.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + hitnotes + + + + + + + + + + \ No newline at end of file diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 00000000..60e556ca --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "hitnotes", + "short_name": "hitnotes", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter application.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +}