diff --git a/resources/[standalone]/menuv/LICENSE b/resources/[standalone]/menuv/LICENSE
new file mode 100644
index 0000000..e72bfdd
--- /dev/null
+++ b/resources/[standalone]/menuv/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
+.
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/README.md b/resources/[standalone]/menuv/README.md
new file mode 100644
index 0000000..5dca70a
--- /dev/null
+++ b/resources/[standalone]/menuv/README.md
@@ -0,0 +1,124 @@
+# MenuV | Standalone Menu for FiveM | NUI Menu
+[![N|CoreV](https://i.imgur.com/iq1llQG.jpg)](https://github.com/ThymonA/menuv)
+
+[![Issues](https://img.shields.io/github/issues/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv/issues)
+[![License](https://img.shields.io/github/license/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv/blob/master/LICENSE)
+[![Forks](https://img.shields.io/github/forks/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv)
+[![Stars](https://img.shields.io/github/stars/ThymonA/menuv.svg?style=for-the-badge)](https://github.com/ThymonA/menuv)
+[![Discord](https://img.shields.io/badge/discord-Tigo%239999-7289da?style=for-the-badge&logo=discord)](https://discordapp.com/users/733686533873467463)
+
+---
+
+**[MenuV](https://github.com/ThymonA/menuv)** is a library written for **[FiveM](https://fivem.net/)** and only uses NUI functionalities. This library allows you to create menus in **[FiveM](https://fivem.net/)**. This project is open-source and you must respect the [license](https://github.com/ThymonA/menuv/blob/master/LICENSE) and the hard work.
+
+## Features
+- Support for simple buttons, sliders, checkboxes, lists and confirms
+- Support for emojis on items
+- Support for custom colors (RGB)
+- Support for all screen resolutions.
+- Item descriptions
+- Rebindable keys
+- Event-based callbacks
+- Uses `2 msec` while menu open and idle.
+- Documentation on [menuv.fivem.io/api/](https://menuv.fivem.io/api/)
+- Themes: **[default](https://i.imgur.com/KSkeiQm.png)** or **[native](https://i.imgur.com/KSkeiQm.png)**
+
+## Compile files
+**[MenuV](https://github.com/ThymonA/menuv)** uses **[VueJS](https://vuejs.org/v2/guide/installation.html#NPM)** and **[TypeScript](https://www.npmjs.com/package/typescript)** with **[NodeJS](https://nodejs.org/en/)**. If you want to use the **`master`** files, you need to build the hole project by doing:
+
+```sh
+npm install
+```
+After you have downloaded/loaded all dependencies, you can build **[MenuV](https://github.com/ThymonA/menuv)** files by executing the following command:
+```sh
+npm run build
+```
+
+After the command is executed you will see a `build` folder containing all the resource files.
+Copy those files to a resource folder called `menuv` or create a symbolic link like that:
+
+### Windows
+
+```batch
+mklink /J "repositoryPath\build" "fxResourcesPath\menuv"
+```
+
+### Linux
+
+```sh
+ln -s "repositoryPath\build" "fxResourcesPath\menuv"
+```
+
+You can also check this tutorial on how to make a link:
+[https://www.howtogeek.com/howto/16226/complete-guide-to-symbolic-links-symlinks-on-windows-or-linux/](https://www.howtogeek.com/howto/16226/complete-guide-to-symbolic-links-symlinks-on-windows-or-linux/)
+
+**When your downloading a [release](https://github.com/ThymonA/menuv/releases), you don't have to follow this step, because all [release](https://github.com/ThymonA/menuv/releases) version are build version.**
+
+## How to use?
+> ⚠️ **example.lua** can't be added in the **menuv** fxmanifest, you must make an seperate resource like: **[menuv_example](https://github.com/ThymonA/menuv/tree/master/example)**
+1. Add `start menuv` to your **server.cfg** before the resources that's uses **menuv**
+2. To use **[MenuV](https://github.com/ThymonA/menuv)** you must add **@menuv/menuv.lua** in your **fxmanifest.lua** file.
+
+```lua
+client_scripts {
+ '@menuv/menuv.lua',
+ 'example.lua'
+}
+```
+
+### Create a menu
+Create a menu by calling the **MenuV:CreateMenu** function.
+```ts
+MenuV:CreateMenu(title: string, subtitle: string, position: string, red: number, green: number, blue: number, texture: string, disctionary: string, namespace: string, theme: string)
+```
+**Example:**
+```lua
+local menu = MenuV:CreateMenu('MenuV', 'Welcome to MenuV', 'topleft', 255, 0, 0, 'size-125', 'default', 'menuv', 'example_namespace', 'native')
+```
+
+### Create menu items
+Create a item by calling **AddButton**, **AddConfirm**, **AddRange**, **AddCheckbox** or **AddSlider** in the created menu
+```ts
+/** CREATE A BUTTON */
+menu:AddButton({ icon: string, label: string, description: string, value: any, disabled: boolean });
+
+/** CREATE A CONFIRM */
+menu:AddConfirm({ icon: string, label: string, description: string, value: boolean, disabled: boolean });
+
+/** CREATE A RANGE */
+menu:AddRange({ icon: string, label: string, description: string, value: number, min: number, max: number, disabled: boolean });
+
+/** CREATE A CHECKBOX */
+menu:AddCheckbox({ icon: string, label: string, description: string, value: boolean, disabled: boolean });
+
+/** CREATE A SLIDER */
+menu:AddSlider({ icon: string, label: string, description: string, value: number, values: [] { label: string, value: any, description: string }, disabled: boolean });
+```
+To see example in practice, see [example.lua](https://github.com/ThymonA/menuv/blob/master/example/example.lua)
+
+### Events
+In **[MenuV](https://github.com/ThymonA/menuv)** you can register event-based callbacks on menu and/or items.
+```ts
+/** REGISTER A EVENT ON MENU */
+menu:On(event: string, callback: function);
+
+/** REGISTER A EVENT ON ANY ITEM */
+item:On(event: string, callback: function);
+```
+
+## Documentation
+Read **[MenuV documentation](https://menuv.fivem.io/api/)**
+
+## License
+Project is written by **[ThymonA](https://github.com/ThymonA/)** and published under
+**GNU General Public License v3.0**
+[Read License](https://github.com/ThymonA/menuv/blob/master/LICENSE)
+
+## Screenshot
+**How is this menu made?** see **[example.lua](https://github.com/ThymonA/menuv/blob/master/example/example.lua)**
+
+
+Default | Native
+:-------|:--------
+![MenuV Default](https://i.imgur.com/xGagIBm.png) | ![MenuV Native](https://i.imgur.com/KSkeiQm.png)
+[Default Theme](https://i.imgur.com/xGagIBm.png) | [Native Theme](https://i.imgur.com/KSkeiQm.png)
diff --git a/resources/[standalone]/menuv/VERSION b/resources/[standalone]/menuv/VERSION
new file mode 100644
index 0000000..13175fd
--- /dev/null
+++ b/resources/[standalone]/menuv/VERSION
@@ -0,0 +1 @@
+1.4.1
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/config.lua b/resources/[standalone]/menuv/config.lua
new file mode 100644
index 0000000..c4c9c87
--- /dev/null
+++ b/resources/[standalone]/menuv/config.lua
@@ -0,0 +1,48 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+Config = {
+ Language = 'en',
+ HideInterval = 250,
+ Sounds = {
+ UP = {
+ type = 'native',
+ name = 'NAV_UP_DOWN',
+ library = 'HUD_FREEMODE_SOUNDSET'
+ },
+ DOWN = {
+ type = 'native',
+ name = 'NAV_UP_DOWN',
+ library = 'HUD_FREEMODE_SOUNDSET'
+ },
+ LEFT = {
+ type = 'native',
+ name = 'NAV_LEFT_RIGHT',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ },
+ RIGHT = {
+ type = 'native',
+ name = 'NAV_LEFT_RIGHT',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ },
+ ENTER = {
+ type = 'native',
+ name = 'SELECT',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ },
+ CLOSE = {
+ type = 'native',
+ name = 'BACK',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ }
+ }
+}
+
+_G.Config = Config
+_ENV.Config = Config
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/dist/assets/css/main.css b/resources/[standalone]/menuv/dist/assets/css/main.css
new file mode 100644
index 0000000..60ad56f
--- /dev/null
+++ b/resources/[standalone]/menuv/dist/assets/css/main.css
@@ -0,0 +1,487 @@
+/*
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+*/
+@import url('https://fonts.googleapis.com/css2?family=Epilogue:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
+
+html,
+body {
+ overflow: hidden;
+ font-family: 'Epilogue', sans-serif;
+ color: white;
+ background-color: transparent;
+}
+
+* .hide,
+html .hide,
+body .hide,
+div .hide,
+.menuv.default.hide {
+ display: none !important;
+ opacity: 0;
+}
+
+.menuv.default {
+ min-width: 20em;
+ max-width: 20em;
+ max-height: 90vh;
+ margin-top: 1em;
+ margin-left: 1em;
+ font-size: 0.85em;
+}
+
+.menuv.default.topcenter {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.menuv.default.topright {
+ margin-right: 1em;
+ margin-left: auto;
+}
+
+.menuv.default.centerleft {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.menuv.default.center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.menuv.default.centerright {
+ position: absolute;
+ top: 50%;
+ right: 1em;
+ transform: translateY(-50%);
+}
+
+.menuv.default.bottomleft {
+ position: absolute;
+ bottom: 1em;
+}
+
+.menuv.default.bottomcenter {
+ position: absolute;
+ bottom: 1em;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.menuv.default.bottomright {
+ position: absolute;
+ bottom: 1em;
+ right: 1em;
+}
+
+.menuv.default.size-100 {
+ zoom: 1;
+}
+.menuv.default.size-110 {
+ zoom: 1.1;
+}
+.menuv.default.size-125 {
+ zoom: 1.25;
+}
+.menuv.default.size-150 {
+ zoom: 1.50;
+}
+.menuv.default.size-175 {
+ zoom: 1.75;
+}
+.menuv.default.size-200 {
+ zoom: 2;
+}
+
+.menuv.default .menuv-header {
+ height: 3.5em;
+ max-height: 3.5em;
+ line-height: 3.5em;
+ width: 100%;
+ text-align: center;
+ letter-spacing: 0.25em;
+ font-size: 1.5em;
+ font-weight: 700;
+ background-color: black;
+ overflow: hidden;
+}
+
+.menuv.default .menuv-header strong {
+ position: relative;
+ z-index: 1;
+ color: white;
+}
+
+.menuv.default .menuv-header .menuv-bg-icon {
+ display: flex;
+ position: fixed;
+ max-height: 3.5em;
+ max-width: 3.5em;
+ overflow: hidden;
+ top: 0.6em;
+ margin-left: 9.8em;
+}
+
+.menuv.default .menuv-header .menuv-bg-icon i,
+.menuv.default .menuv-header .menuv-bg-icon svg {
+ font-size: 5em;
+ opacity: 0.5;
+ color: blue;
+}
+
+.menuv.default .menuv-subheader {
+ background-color: blue;
+ text-align: left;
+ font-weight: 700;
+ font-size: 0.9em;
+ line-height: 2.5em;
+ text-transform: uppercase;
+ padding-left: 1em;
+ height: 2.5em;
+ max-height: 2.5em;
+}
+
+.menuv.default .menuv-items {
+ background-color: rgba(0, 0, 0, 0.65);
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+ max-height: 50.75vh;
+ overflow: hidden;
+ color: white;
+}
+
+.menuv.default.size-100 .menuv-items {
+ max-height: 66.1vh;
+}
+
+.menuv.default.size-110 .menuv-items {
+ max-height: 59.2vh;
+}
+
+.menuv.default.size-125 .menuv-items {
+ max-height: 50.2vh;
+}
+
+.menuv.default.size-150 .menuv-items {
+ max-height: 45.8vh;
+}
+
+.menuv.default.size-175 .menuv-items {
+ max-height: 39vh;
+}
+
+.menuv.default.size-200 .menuv-items {
+ max-height: 32.2vh;
+}
+
+.menuv.default .menuv-items .menuv-item {
+ padding: 0.25em 0.50em;
+ margin: 0;
+ font-size: 0.9em;
+ max-height: auto;
+ height: auto;
+ min-height: 2em;
+ vertical-align: middle;
+ line-height: normal;
+ color: white;
+ width: 100%;
+ min-width: 20em;
+ max-width: 30em;
+ border-top: none !important;
+}
+
+.menuv.default .menuv-items .menuv-item i,
+.menuv.default .menuv-items .menuv-item svg {
+ float: right;
+ margin-top: 0.125em;
+ font-size: 1.2em;
+}
+
+.menuv.default .menuv-items .menuv-item.active {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.menuv.default .menuv-items .menuv-item.disabled {
+ opacity: 0.75;
+ background: #383838;
+ text-decoration: line-through;
+}
+
+.menuv.default .menuv-items .menuv-item.active i,
+.menuv.default .menuv-items .menuv-item.active svg {
+ color: black;
+}
+
+.menuv.default .menuv-items .item-title {
+ word-break: break-all;
+}
+
+.menuv.default .menuv-items .menuv-item.active .item-title {
+ font-weight: bold;
+}
+
+.menuv.default .menuv-items span.menuv-icon {
+ margin-left: 2.5px;
+ margin-right: 5px;
+ border-right: 1px solid white;
+ padding-right: 5px;
+ float: left;
+ width: 2em;
+ text-align: center;
+}
+
+.menuv.default .menuv-items .flex-left {
+ justify-content: left;
+}
+
+.menuv.default .menuv-items span.menuv-title {
+ word-break: break-all;
+ display: inline-block;
+ overflow: hidden;
+ max-height: 2em;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ max-width: 14em;
+}
+
+.menuv.default .menuv-items .item-icon {
+ width: 2.5em;
+ max-width: 2.5em;
+ margin-right: 5px;
+}
+
+.menuv.default .menuv-items .menuv-item {
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+
+ -ms-flex-align: center;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+
+ align-items: center;
+ border-top: none !important;
+}
+
+.menuv.default .menuv-items .menuv-item.active span.menuv-icon {
+ border-right: 1px solid black;
+}
+
+.menuv.default .menuv-items span.menuv-options {
+ float: right;
+ font-size: 0.9em;
+ line-height: 1.85em;
+ text-transform: uppercase;
+}
+
+.menuv.default .menuv-items .menuv-item.active span.menuv-options {
+ font-weight: 700;
+}
+
+.menuv.default .menuv-items span.menuv-options i,
+.menuv.default .menuv-items span.menuv-options svg {
+ float: unset;
+ font-size: unset;
+}
+
+.menuv.default .menuv-items span.menuv-options i:first-child,
+.menuv.default .menuv-items span.menuv-options svg:first-child {
+ margin-right: 0.25em;
+}
+
+.menuv.default .menuv-items span.menuv-options i:last-child,
+.menuv.default .menuv-items span.menuv-options svg:last-child {
+ margin-left: 0.25em;
+}
+
+.menuv.default .menuv-items span.menuv-options span.menuv-btn {
+ background-color: white;
+ color: black;
+ padding: 0.25em 0.5em;
+ margin: 0.125em;
+ font-weight: bold;
+ font-weight: 500;
+ border-radius: 0.125em;
+}
+
+.menuv.default .menuv-items .menuv-item span.menuv-options span.menuv-btn {
+ background-color: transparent;
+ color: white;
+}
+
+.menuv.default .menuv-items .menuv-item.active span.menuv-options span.menuv-btn {
+ background-color: black;
+ color: white;
+}
+
+.menuv.default .menuv-items span.menuv-options span.menuv-btn.active {
+ background-color: blue;
+ color: white;
+ font-weight: 700;
+}
+
+.menuv.default .menuv-items .menuv-item.active span.menuv-options span.menuv-btn.active {
+ background-color: blue;
+ color: white;
+}
+
+.menuv.default .menuv-items input[type="range"] {
+ display: flex;
+ float: right;
+ -webkit-appearance: none;
+ max-width: 6.5em;
+}
+
+.menuv.default .menuv-items input[type="range"]:focus {
+ outline: none;
+}
+
+.menuv.default .menuv-items input[type="range"]::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 5px;
+ cursor: pointer;
+ box-shadow: 0px 0px 0px #000000;
+ background: blue;
+ border-radius: 0;
+ border: 0px solid #000000;
+}
+
+.menuv.default .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-runnable-track {
+ background: black;
+}
+
+.menuv.default .menuv-items input[type="range"]::-webkit-slider-thumb {
+ box-shadow: 0px 0px 0px #000000;
+ height: 18px;
+ width: 5px;
+ border-radius: 0;
+ border: 1px solid white;
+ background: rgba(255, 255, 255, 1);
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -7px;
+}
+
+.menuv.default .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-thumb {
+ background: blue;
+ border: 1px solid rgba(0, 0, 255, 0.25);
+}
+
+.menuv.default .menuv-items input[type="range"]:focus::-webkit-slider-runnable-track {
+ background: blue;
+}
+
+.menuv.default .menuv-items .menuv-item.active input[type="range"]:focus::-webkit-slider-runnable-track {
+ background: black;
+}
+
+.menuv.default .menuv-items .menuv-desc {
+ display: none;
+ opacity: 0;
+ background-color: rgba(0, 0, 0, 0.65);
+ color: white;
+ position: absolute;
+ width: 100%;
+ max-width: 17.5em;
+ margin-left: 17.5em;
+ margin-top: -0.25em;
+ font-weight: 400;
+ font-size: 0.9em;
+ padding: 0.75em 1em;
+ line-height: 1.25em;
+ border-left: 0.375em solid blue;
+}
+
+.menuv.default .menuv-items .menuv-item.active .menuv-desc {
+ display: initial;
+ opacity: 1;
+}
+
+.menuv.default .menuv-items .menuv-desc strong {
+ color: white;
+}
+
+.menuv.default .menuv-items .menuv-desc table {
+ margin-left: -0.75em;
+ width: calc(100% + 0.75em);
+}
+
+.menuv.default .menuv-items .menuv-desc table th {
+ color: white;
+ padding: 2px 5px;
+}
+
+.menuv.default .menuv-items .menuv-desc table td {
+ padding: 2px 5px;
+}
+
+.menuv.default .menuv-items .menuv-label {
+ float: right;
+ font-size: 1.125em;
+ font-weight: 800;
+}
+
+.menuv.default .menuv-pagination {
+ padding: 0.5em;
+ max-width: 20em;
+ width: 100%;
+ text-align: center;
+ position: relative;
+ border-top: 2px solid white;
+ margin-top: 1em;
+}
+
+.menuv.default .menuv-pagination .menu-pagination-option {
+ display: inline-block;
+ height: 1.5em;
+ width: 3em;
+ background-color: white;
+ color: black;
+ text-align: center;
+ border-radius: 2.5px;
+ margin-left: 0.25em;
+ margin-right: 0.25em;
+ font-size: 0.8em;
+}
+
+.menuv.default .menuv-pagination .menu-pagination-option.active {
+ background-color: red;
+ color: white;
+}
+
+.menuv.default .menuv-pagination .menu-pagination-ellipsis {
+ display: inline-block;
+ height: 1.5em;
+ width: 1.5em;
+ background-color: transparent;
+ color: white;
+ text-align: center;
+}
+
+.menuv.default .menuv-description {
+ background-color: rgba(0, 0, 0, 0.65);
+ width: 100%;
+ max-width: 20em;
+ padding: 0.5em 1em;
+ margin-top: 0.5em;
+ text-align: center;
+ text-transform: uppercase;
+}
+
+.menuv.default .menuv-description strong {
+ color: white;
+ font-size: 0.8em;
+ font-weight: 400;
+}
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/dist/assets/css/native_theme.css b/resources/[standalone]/menuv/dist/assets/css/native_theme.css
new file mode 100644
index 0000000..fb1110f
--- /dev/null
+++ b/resources/[standalone]/menuv/dist/assets/css/native_theme.css
@@ -0,0 +1,507 @@
+/*
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+*/
+@font-face {
+ font-family: 'SignPainter';
+ font-style: normal;
+ font-weight: normal;
+ src: local('../fonts/SignPainter'), url('../fonts/SignPainterHouseScript.woff') format('woff');
+}
+
+@font-face {
+ font-family: 'TTCommons';
+ font-style: normal;
+ font-weight: normal;
+ src: local('../fonts/TTCommons'), url('../fonts/TTCommons.woff') format('woff');
+}
+
+html,
+body {
+ overflow: hidden;
+ color: white;
+ background-color: transparent;
+}
+
+* .hide,
+html .hide,
+body .hide,
+div .hide,
+.menuv.native.hide {
+ display: none !important;
+ opacity: 0;
+}
+
+.menuv.native {
+ min-width: 30em;
+ max-width: 30em;
+ max-height: 90vh;
+ margin-top: 1em;
+ margin-left: 1em;
+ font-size: 0.85em;
+}
+
+.menuv.native.topcenter {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.menuv.native.topright {
+ margin-right: 1em;
+ margin-left: auto;
+}
+
+.menuv.native.centerleft {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.menuv.native.center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.menuv.native.centerright {
+ position: absolute;
+ top: 50%;
+ right: 1em;
+ transform: translateY(-50%);
+}
+
+.menuv.native.bottomleft {
+ position: absolute;
+ bottom: 1em;
+}
+
+.menuv.native.bottomcenter {
+ position: absolute;
+ bottom: 1em;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.menuv.native.bottomright {
+ position: absolute;
+ bottom: 1em;
+ right: 1em;
+}
+
+.menuv.native.size-100 {
+ zoom: 1;
+}
+.menuv.native.size-110 {
+ zoom: 1.1;
+}
+.menuv.native.size-125 {
+ zoom: 1.25;
+}
+.menuv.native.size-150 {
+ zoom: 1.50;
+}
+.menuv.native.size-175 {
+ zoom: 1.75;
+}
+.menuv.native.size-200 {
+ zoom: 2;
+}
+
+.menuv.native .menuv-header {
+ height: 2em;
+ max-height: 4.25em;
+ line-height: 2.25em;
+ width: 100%;
+ text-align: center;
+ letter-spacing: auto;
+ font-size: 3.5em;
+ font-weight: normal;
+ font-family: 'SignPainter';
+ background-color: black;
+ overflow: hidden;
+}
+
+.menuv.native .menuv-header strong {
+ position: relative;
+ z-index: 1;
+ color: white;
+ max-width: 1em;
+ font-weight: normal;
+}
+
+.menuv.native .menuv-header .menuv-bg-icon {
+ display: flex;
+ position: fixed;
+ max-height: 4.25em;
+ max-width: 4.25em;
+ overflow: hidden;
+ top: 0.6em;
+ margin-left: 9.8em;
+}
+
+.menuv.native .menuv-header .menuv-bg-icon i,
+.menuv.native .menuv-header .menuv-bg-icon svg {
+ font-size: 5em;
+ opacity: 0.5;
+ color: blue;
+}
+
+.menuv.native .menuv-subheader {
+ background-color: black !important;
+ text-align: left;
+ font-weight: 500;
+ font-size: 1.125em;
+ line-height: auto;
+ text-transform: uppercase;
+ padding-top: 0.125em;
+ padding-bottom: 0.375em;
+ padding-left: 0.5em;
+ height: auto;
+ color: #2e69bb !important;
+}
+
+.menuv.native .menuv-items {
+ background-color: rgba(0, 0, 0, 0.65);
+ padding-bottom: 0.5em;
+ max-height: 50.75vh;
+ overflow: hidden;
+ color: white;
+}
+
+.menuv.native.size-100 .menuv-items {
+ max-height: 66.1vh;
+}
+
+.menuv.native.size-110 .menuv-items {
+ max-height: 59.2vh;
+}
+
+.menuv.native.size-125 .menuv-items {
+ max-height: 50.2vh;
+}
+
+.menuv.native.size-150 .menuv-items {
+ max-height: 45.8vh;
+}
+
+.menuv.native.size-175 .menuv-items {
+ max-height: 39vh;
+}
+
+.menuv.native.size-200 .menuv-items {
+ max-height: 32.2vh;
+}
+
+.menuv.native .menuv-items .menuv-item {
+ padding: 0.25em 0.50em;
+ margin: 0;
+ font-size: 1em;
+ max-height: auto;
+ height: auto;
+ line-height: 1.25em;
+ color: white;
+ width: 100%;
+ min-width: 100%;
+ max-width: 30em;
+}
+
+.menuv.native .menuv-items .flex-left {
+ justify-content: left;
+}
+
+.menuv.native .menuv-items .item-title {
+ font-family: 'TTCommons';
+ font-weight: 400;
+ font-size: 1.35em;
+ padding-top: 0.25em;
+ word-break: break-all;
+}
+
+.menuv.native .menuv-items .item-icon {
+ width: 2.5em;
+ max-width: 2.5em;
+ margin-right: 5px;
+}
+
+.menuv.native .menuv-items .menuv-item.disabled .item-icon {
+ text-decoration: none !important;
+}
+
+.menuv.native .menuv-items .menuv-item i,
+.menuv.native .menuv-items .menuv-item svg {
+ float: right;
+ margin-top: 0.125em;
+ font-size: 1.2em;
+}
+
+.menuv.native .menuv-items .menuv-item.active {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.menuv.native .menuv-items .menuv-item.disabled {
+ opacity: 0.75;
+ background: #383838;
+}
+
+.menuv.native .menuv-items .menuv-item.active i,
+.menuv.native .menuv-items .menuv-item.active svg {
+ color: black;
+}
+
+.menuv.native .menuv-items span.menuv-icon {
+ margin-left: 2.5px;
+ margin-right: 5px;
+ border-right: 1px solid white;
+ padding-right: 5px;
+ float: left;
+ width: 2em;
+ text-align: center;
+}
+
+.menuv.native .menuv-items span.menuv-title {
+ word-break: break-all;
+ display: inline-block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-size: 1.25em;
+ font-weight: 600;
+ letter-spacing: normal;
+ padding: none;
+ margin: none;
+}
+
+.menuv.native .menuv-items .menuv-item.active span.menuv-icon {
+ border-right: 1px solid black;
+}
+
+.menuv.native .menuv-items .menuv-item {
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+
+ -ms-flex-align: center;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+
+ align-items: center;
+ border-top: none !important;
+}
+
+.menuv.native .menuv-items span.menuv-options {
+ float: right;
+ font-size: 0.9em;
+ vertical-align: middle;
+ text-transform: uppercase;
+}
+
+.menuv.native .menuv-items .menuv-item.active span.menuv-options {
+ font-weight: 700;
+}
+
+.menuv.native .menuv-items span.menuv-options i,
+.menuv.native .menuv-items span.menuv-options svg {
+ float: unset;
+ font-size: unset;
+}
+
+.menuv.native .menuv-items span.menuv-options i:first-child,
+.menuv.native .menuv-items span.menuv-options svg:first-child {
+ margin-right: 0.25em;
+}
+
+.menuv.native .menuv-items span.menuv-options i:last-child,
+.menuv.native .menuv-items span.menuv-options svg:last-child {
+ margin-left: 0.25em;
+}
+
+.menuv.native .menuv-items span.menuv-options span.menuv-btn {
+ background-color: white;
+ color: black;
+ padding: 0.25em 0.5em;
+ margin: 0.125em;
+ font-weight: bold;
+ font-weight: 500;
+ border-radius: 0.125em;
+}
+
+.menuv.native .menuv-items .menuv-item span.menuv-options span.menuv-btn {
+ background-color: transparent;
+ color: white;
+}
+
+.menuv.native .menuv-items .menuv-item.active span.menuv-options span.menuv-btn {
+ background-color: black;
+ color: white;
+}
+
+.menuv.native .menuv-items span.menuv-options span.menuv-btn.active {
+ background-color: blue;
+ color: white;
+ font-weight: 700;
+}
+
+.menuv.native .menuv-items .menuv-item.active span.menuv-options span.menuv-btn.active {
+ background-color: blue;
+ color: white;
+}
+
+.menuv.native .menuv-items input[type="range"] {
+ display: flex;
+ float: right;
+ -webkit-appearance: none;
+ max-width: 6.5em;
+ margin-top: 0.15em;
+}
+
+.menuv.native .menuv-items input[type="range"]:focus {
+ outline: none;
+}
+
+.menuv.native .menuv-items input[type="range"]::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 5px;
+ cursor: pointer;
+ box-shadow: 0px 0px 0px #000000;
+ background: blue;
+ border-radius: 0;
+ border: 0px solid #000000;
+}
+
+.menuv.native .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-runnable-track {
+ background: black;
+}
+
+.menuv.native .menuv-items input[type="range"]::-webkit-slider-thumb {
+ box-shadow: 0px 0px 0px #000000;
+ height: 18px;
+ width: 5px;
+ border-radius: 0;
+ border: 1px solid white;
+ background: rgba(255, 255, 255, 1);
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -7px;
+}
+
+.menuv.native .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-thumb {
+ background: blue;
+ border: 1px solid rgba(0, 0, 255, 0.25);
+}
+
+.menuv.native .menuv-items input[type="range"]:focus::-webkit-slider-runnable-track {
+ background: blue;
+}
+
+.menuv.native .menuv-items .menuv-item.active input[type="range"]:focus::-webkit-slider-runnable-track {
+ background: black;
+}
+
+.menuv.native .menuv-items .menuv-desc {
+ display: none;
+ opacity: 0;
+ background-color: rgba(0, 0, 0, 0.65);
+ color: white;
+ position: absolute;
+ width: 100%;
+ max-width: 17.5em;
+ margin-left: 17.5em;
+ margin-top: -0.25em;
+ font-weight: 400;
+ font-size: 0.9em;
+ padding: 0.75em 1em;
+ line-height: 1.25em;
+ border-left: 0.375em solid blue;
+}
+
+.menuv.native .menuv-items .menuv-item.active .menuv-desc {
+ display: initial;
+ opacity: 1;
+}
+
+.menuv.native .menuv-items .menuv-desc strong {
+ color: white;
+}
+
+.menuv.native .menuv-items .menuv-desc table {
+ margin-left: -0.75em;
+ width: calc(100% + 0.75em);
+}
+
+.menuv.native .menuv-items .menuv-desc table th {
+ color: white;
+ padding: 2px 5px;
+}
+
+.menuv.native .menuv-items .menuv-desc table td {
+ padding: 2px 5px;
+}
+
+.menuv.native .menuv-items .menuv-label {
+ float: right;
+ font-size: 1.125em;
+ font-weight: 800;
+}
+
+.menuv.native .menuv-pagination {
+ padding: 0.5em;
+ max-width: 20em;
+ width: 100%;
+ text-align: center;
+ position: relative;
+ border-top: 2px solid white;
+ margin-top: 1em;
+}
+
+.menuv.native .menuv-pagination .menu-pagination-option {
+ display: inline-block;
+ height: 1.5em;
+ width: 3em;
+ background-color: white;
+ color: black;
+ text-align: center;
+ border-radius: 2.5px;
+ margin-left: 0.25em;
+ margin-right: 0.25em;
+ font-size: 0.8em;
+}
+
+.menuv.native .menuv-pagination .menu-pagination-option.active {
+ background-color: red;
+ color: white;
+}
+
+.menuv.native .menuv-pagination .menu-pagination-ellipsis {
+ display: inline-block;
+ height: 1.5em;
+ width: 1.5em;
+ background-color: transparent;
+ color: white;
+ text-align: center;
+}
+
+.menuv.native .menuv-description {
+ border-top: 2px solid black;
+ background-color: rgba(0, 0, 0, 0.65);
+ width: 100%;
+ max-width: 30em;
+ padding: 0.5em 1em;
+ margin-top: 0.5em;
+ text-align: left;
+}
+
+.menuv.native .menuv-description strong {
+ color: white;
+ font-family: 'TTCommons';
+ font-weight: 400;
+ font-size: 1.15em;
+}
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/dist/assets/fonts/SignPainterHouseScript.woff b/resources/[standalone]/menuv/dist/assets/fonts/SignPainterHouseScript.woff
new file mode 100644
index 0000000..3cc8ffb
Binary files /dev/null and b/resources/[standalone]/menuv/dist/assets/fonts/SignPainterHouseScript.woff differ
diff --git a/resources/[standalone]/menuv/dist/assets/fonts/TTCommons.woff b/resources/[standalone]/menuv/dist/assets/fonts/TTCommons.woff
new file mode 100644
index 0000000..0b4fb47
Binary files /dev/null and b/resources/[standalone]/menuv/dist/assets/fonts/TTCommons.woff differ
diff --git a/resources/[standalone]/menuv/dist/assets/js/menuv.js b/resources/[standalone]/menuv/dist/assets/js/menuv.js
new file mode 100644
index 0000000..2f3674d
--- /dev/null
+++ b/resources/[standalone]/menuv/dist/assets/js/menuv.js
@@ -0,0 +1,2 @@
+/*! For license information please see menuv.js.LICENSE.txt */
+(()=>{var t={81:function(t){t.exports=function(){"use strict";function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}function e(){return(e=Object.assign||function(t){for(var e=1;e=.001?function(t,e,n,r){for(var i=0;i<4;++i){var o=u(e,n,r);if(0===o)return e;e-=(a(e,n,r)-t)/o}return e}(e,c,t,i):0===l?c:function(t,e,n,r,i){var o,s,u=0;do{(o=a(s=e+(n-e)/2,r,i)-t)>0?n=s:e=s}while(Math.abs(o)>1e-7&&++u<10);return s}(e,r,r+n,t,i)}return function(t){return 0===t?0:1===t?1:a(d(t),e,o)}},d={ease:[.25,.1,.25,1],linear:[0,0,1,1],"ease-in":[.42,0,1,1],"ease-out":[0,0,.58,1],"ease-in-out":[.42,0,.58,1]},f=!1;try{var p=Object.defineProperty({},"passive",{get:function(){f=!0}});window.addEventListener("test",null,p)}catch(t){}var h=function(t){return"string"!=typeof t?t:document.querySelector(t)},v=function(t,e,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{passive:!1};e instanceof Array||(e=[e]);for(var i=0;i2&&void 0!==arguments[2]?arguments[2]:{};if("object"===t(y)?w=y:"number"==typeof y&&(w.duration=y),!(e=h(m)))return console.warn("[vue-scrollto warn]: Trying to scroll to an element that is not on the page: "+m);if(n=h(w.container||g.container),r=w.hasOwnProperty("duration")?w.duration:g.duration,o=w.hasOwnProperty("lazy")?w.lazy:g.lazy,i=w.easing||g.easing,s=w.hasOwnProperty("offset")?w.offset:g.offset,a=w.hasOwnProperty("force")?!1!==w.force:g.force,u=w.hasOwnProperty("cancelable")?!1!==w.cancelable:g.cancelable,c=w.onStart||g.onStart,f=w.onDone||g.onDone,p=w.onCancel||g.onCancel,b=void 0===w.x?g.x:w.x,E=void 0===w.y?g.y:w.y,"function"==typeof s&&(s=s(e,n)),x=j(n),T=M(n),U(),k=!1,!a){var A="body"===n.tagName.toLowerCase()?document.documentElement.clientHeight||window.innerHeight:n.offsetHeight,$=T,P=$+A,R=O-s,I=R+e.offsetHeight;if(R>=$&&I<=P)return void(f&&f(e))}if(c&&c(e),S||C)return"string"==typeof i&&(i=d[i]||d.ease),L=l.apply(l,i),v(n,_,D,{passive:!0}),window.requestAnimationFrame(F),function(){N=null,k=!0};f&&f(e)}},x=E(),w=[];function T(t){var e=function(t){for(var e=0;e{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=Object.freeze({});function e(t){return null==t}function r(t){return null!=t}function i(t){return!0===t}function o(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function s(t){return null!==t&&"object"==typeof t}var a=Object.prototype.toString;function u(t){return"[object Object]"===a.call(t)}function c(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function l(t){return r(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function d(t){return null==t?"":Array.isArray(t)||u(t)&&t.toString===a?JSON.stringify(t,null,2):String(t)}function f(t){var e=parseFloat(t);return isNaN(e)?t:e}function p(t,e){for(var n=Object.create(null),r=t.split(","),i=0;i-1)return t.splice(n,1)}}var m=Object.prototype.hasOwnProperty;function y(t,e){return m.call(t,e)}function _(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var g=/-(\w)/g,b=_((function(t){return t.replace(g,(function(t,e){return e?e.toUpperCase():""}))})),E=_((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),x=/\B([A-Z])/g,w=_((function(t){return t.replace(x,"-$1").toLowerCase()})),T=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var r=arguments.length;return r?r>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function O(t,e){e=e||0;for(var n=t.length-e,r=new Array(n);n--;)r[n]=t[n+e];return r}function C(t,e){for(var n in e)t[n]=e[n];return t}function S(t){for(var e={},n=0;n0,W=B&&B.indexOf("edge/")>0,q=(B&&B.indexOf("android"),B&&/iphone|ipad|ipod|ios/.test(B)||"ios"===V),Y=(B&&/chrome\/\d+/.test(B),B&&/phantomjs/.test(B),B&&B.match(/firefox\/(\d+)/)),J={}.watch,Z=!1;if(H)try{var Q={};Object.defineProperty(Q,"passive",{get:function(){Z=!0}}),window.addEventListener("test-passive",null,Q)}catch(t){}var tt=function(){return void 0===U&&(U=!H&&!z&&void 0!==n.g&&n.g.process&&"server"===n.g.process.env.VUE_ENV),U},et=H&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function nt(t){return"function"==typeof t&&/native code/.test(t.toString())}var rt,it="undefined"!=typeof Symbol&&nt(Symbol)&&"undefined"!=typeof Reflect&&nt(Reflect.ownKeys);rt="undefined"!=typeof Set&&nt(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var ot=k,st=0,at=function(){this.id=st++,this.subs=[]};at.prototype.addSub=function(t){this.subs.push(t)},at.prototype.removeSub=function(t){v(this.subs,t)},at.prototype.depend=function(){at.target&&at.target.addDep(this)},at.prototype.notify=function(){for(var t=this.subs.slice(),e=0,n=t.length;e-1)if(o&&!y(i,"default"))s=!1;else if(""===s||s===w(t)){var u=jt(String,i.type);(u<0||a0&&(ue((a=ce(a,(n||"")+"_"+s))[0])&&ue(c)&&(l[u]=ht(c.text+a[0].text),a.shift()),l.push.apply(l,a)):o(a)?ue(c)?l[u]=ht(c.text+a):""!==a&&l.push(ht(a)):ue(a)&&ue(c)?l[u]=ht(c.text+a.text):(i(t._isVList)&&r(a.tag)&&e(a.key)&&r(n)&&(a.key="__vlist"+n+"_"+s+"__"),l.push(a)));return l}function le(t,e){if(t){for(var n=Object.create(null),r=it?Reflect.ownKeys(t):Object.keys(t),i=0;i0,s=e?!!e.$stable:!o,a=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(s&&r&&r!==t&&a===r.$key&&!o&&!r.$hasNormal)return r;for(var u in i={},e)e[u]&&"$"!==u[0]&&(i[u]=he(n,u,e[u]))}else i={};for(var c in n)c in i||(i[c]=ve(n,c));return e&&Object.isExtensible(e)&&(e._normalized=i),j(i,"$stable",s),j(i,"$key",a),j(i,"$hasNormal",o),i}function he(t,e,n){var r=function(){var t=arguments.length?n.apply(null,arguments):n({});return(t=t&&"object"==typeof t&&!Array.isArray(t)?[t]:ae(t))&&(0===t.length||1===t.length&&t[0].isComment)?void 0:t};return n.proxy&&Object.defineProperty(t,e,{get:r,enumerable:!0,configurable:!0}),r}function ve(t,e){return function(){return t[e]}}function me(t,e){var n,i,o,a,u;if(Array.isArray(t)||"string"==typeof t)for(n=new Array(t.length),i=0,o=t.length;idocument.createEvent("Event").timeStamp&&(cn=function(){return ln.now()})}function dn(){var t,e;for(un=cn(),sn=!0,en.sort((function(t,e){return t.id-e.id})),an=0;anan&&en[n].id>t.id;)n--;en.splice(n+1,0,t)}else en.push(t);on||(on=!0,Zt(dn))}}(this)},pn.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||s(t)||this.deep){var e=this.value;if(this.value=t,this.user)try{this.cb.call(this.vm,t,e)}catch(t){Ut(t,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,t,e)}}},pn.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},pn.prototype.depend=function(){for(var t=this.deps.length;t--;)this.deps[t].depend()},pn.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||v(this.vm._watchers,this);for(var t=this.deps.length;t--;)this.deps[t].removeSub(this);this.active=!1}};var hn={enumerable:!0,configurable:!0,get:k,set:k};function vn(t,e,n){hn.get=function(){return this[e][n]},hn.set=function(t){this[e][n]=t},Object.defineProperty(t,n,hn)}var mn={lazy:!0};function yn(t,e,n){var r=!tt();"function"==typeof n?(hn.get=r?_n(e):gn(n),hn.set=k):(hn.get=n.get?r&&!1!==n.cache?_n(e):gn(n.get):k,hn.set=n.set||k),Object.defineProperty(t,e,hn)}function _n(t){return function(){var e=this._computedWatchers&&this._computedWatchers[t];if(e)return e.dirty&&e.evaluate(),at.target&&e.depend(),e.value}}function gn(t){return function(){return t.call(this,this)}}function bn(t,e,n,r){return u(n)&&(r=n,n=n.handler),"string"==typeof n&&(n=t[n]),t.$watch(e,n,r)}var En=0;function xn(t){var e=t.options;if(t.super){var n=xn(t.super);if(n!==t.superOptions){t.superOptions=n;var r=function(t){var e,n=t.options,r=t.sealedOptions;for(var i in n)n[i]!==r[i]&&(e||(e={}),e[i]=n[i]);return e}(t);r&&C(t.extendOptions,r),(e=t.options=Pt(n,t.extendOptions)).name&&(e.components[e.name]=t)}}return e}function wn(t){this._init(t)}function Tn(t){return t&&(t.Ctor.options.name||t.tag)}function On(t,e){return Array.isArray(t)?t.indexOf(e)>-1:"string"==typeof t?t.split(",").indexOf(e)>-1:(n=t,!("[object RegExp]"!==a.call(n))&&t.test(e));var n}function Cn(t,e){var n=t.cache,r=t.keys,i=t._vnode;for(var o in n){var s=n[o];if(s){var a=Tn(s.componentOptions);a&&!e(a)&&Sn(n,o,r,i)}}}function Sn(t,e,n,r){var i=t[e];!i||r&&i.tag===r.tag||i.componentInstance.$destroy(),t[e]=null,v(n,e)}!function(e){e.prototype._init=function(e){var n=this;n._uid=En++,n._isVue=!0,e&&e._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),r=e._parentVnode;n.parent=e.parent,n._parentVnode=r;var i=r.componentOptions;n.propsData=i.propsData,n._parentListeners=i.listeners,n._renderChildren=i.children,n._componentTag=i.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(n,e):n.$options=Pt(xn(n.constructor),e||{},n),n._renderProxy=n,n._self=n,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(n),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&We(t,e)}(n),function(e){e._vnode=null,e._staticTrees=null;var n=e.$options,r=e.$vnode=n._parentVnode,i=r&&r.context;e.$slots=de(n._renderChildren,i),e.$scopedSlots=t,e._c=function(t,n,r,i){return je(e,t,n,r,i,!1)},e.$createElement=function(t,n,r,i){return je(e,t,n,r,i,!0)};var o=r&&r.data;wt(e,"$attrs",o&&o.attrs||t,null,!0),wt(e,"$listeners",n._parentListeners||t,null,!0)}(n),tn(n,"beforeCreate"),function(t){var e=le(t.$options.inject,t);e&&(bt(!1),Object.keys(e).forEach((function(n){wt(t,n,e[n])})),bt(!0))}(n),function(t){t._watchers=[];var e=t.$options;e.props&&function(t,e){var n=t.$options.propsData||{},r=t._props={},i=t.$options._propKeys=[];t.$parent&&bt(!1);var o=function(o){i.push(o);var s=It(o,e,n,t);wt(r,o,s),o in t||vn(t,"_props",o)};for(var s in e)o(s);bt(!0)}(t,e.props),e.methods&&function(t,e){for(var n in t.$options.props,e)t[n]="function"!=typeof e[n]?k:T(e[n],t)}(t,e.methods),e.data?function(t){var e=t.$options.data;u(e=t._data="function"==typeof e?function(t,e){ct();try{return t.call(e,e)}catch(t){return Ut(t,e,"data()"),{}}finally{lt()}}(e,t):e||{})||(e={});for(var n,r=Object.keys(e),i=t.$options.props,o=(t.$options.methods,r.length);o--;){var s=r[o];i&&y(i,s)||(n=void 0,36===(n=(s+"").charCodeAt(0))||95===n)||vn(t,"_data",s)}xt(e,!0)}(t):xt(t._data={},!0),e.computed&&function(t,e){var n=t._computedWatchers=Object.create(null),r=tt();for(var i in e){var o=e[i],s="function"==typeof o?o:o.get;r||(n[i]=new pn(t,s||k,k,mn)),i in t||yn(t,i,o)}}(t,e.computed),e.watch&&e.watch!==J&&function(t,e){for(var n in e){var r=e[n];if(Array.isArray(r))for(var i=0;i1?O(n):n;for(var r=O(arguments,1),i='event handler for "'+t+'"',o=0,s=n.length;oparseInt(this.max)&&Sn(s,a[0],a,this._vnode)),e.data.keepAlive=!0}return e||t&&t[0]}}};!function(t){var e={get:function(){return M}};Object.defineProperty(t,"config",e),t.util={warn:ot,extend:C,mergeOptions:Pt,defineReactive:wt},t.set=Tt,t.delete=Ot,t.nextTick=Zt,t.observable=function(t){return xt(t),t},t.options=Object.create(null),I.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,C(t.options.components,An),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=O(arguments,1);return n.unshift(this),"function"==typeof t.install?t.install.apply(t,n):"function"==typeof t&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Pt(this.options,t),this}}(t),function(t){t.cid=0;var e=1;t.extend=function(t){t=t||{};var n=this,r=n.cid,i=t._Ctor||(t._Ctor={});if(i[r])return i[r];var o=t.name||n.options.name,s=function(t){this._init(t)};return(s.prototype=Object.create(n.prototype)).constructor=s,s.cid=e++,s.options=Pt(n.options,t),s.super=n,s.options.props&&function(t){var e=t.options.props;for(var n in e)vn(t.prototype,"_props",n)}(s),s.options.computed&&function(t){var e=t.options.computed;for(var n in e)yn(t.prototype,n,e[n])}(s),s.extend=n.extend,s.mixin=n.mixin,s.use=n.use,I.forEach((function(t){s[t]=n[t]})),o&&(s.options.components[o]=s),s.superOptions=n.options,s.extendOptions=t,s.sealedOptions=C({},s.options),i[r]=s,s}}(t),function(t){I.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&u(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&"function"==typeof n&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(wn),Object.defineProperty(wn.prototype,"$isServer",{get:tt}),Object.defineProperty(wn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(wn,"FunctionalRenderContext",{value:Ne}),wn.version="2.6.12";var $n=p("style,class"),Nn=p("input,textarea,option,select,progress"),Ln=p("contenteditable,draggable,spellcheck"),Pn=p("events,caret,typing,plaintext-only"),Rn=p("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),In="http://www.w3.org/1999/xlink",Dn=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Mn=function(t){return Dn(t)?t.slice(6,t.length):""},jn=function(t){return null==t||!1===t};function Un(t,e){return{staticClass:Fn(t.staticClass,e.staticClass),class:r(t.class)?[t.class,e.class]:e.class}}function Fn(t,e){return t?e?t+" "+e:t:e||""}function Xn(t){return Array.isArray(t)?function(t){for(var e,n="",i=0,o=t.length;i-1?lr(t,e,n):Rn(e)?jn(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):Ln(e)?t.setAttribute(e,function(t,e){return jn(e)||"false"===e?"false":"contenteditable"===t&&Pn(e)?e:"true"}(e,n)):Dn(e)?jn(n)?t.removeAttributeNS(In,Mn(e)):t.setAttributeNS(In,e,n):lr(t,e,n)}function lr(t,e,n){if(jn(n))t.removeAttribute(e);else{if(G&&!K&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var r=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",r)};t.addEventListener("input",r),t.__ieph=!0}t.setAttribute(e,n)}}var dr={create:ur,update:ur};function fr(t,n){var i=n.elm,o=n.data,s=t.data;if(!(e(o.staticClass)&&e(o.class)&&(e(s)||e(s.staticClass)&&e(s.class)))){var a=function(t){for(var e=t.data,n=t,i=t;r(i.componentInstance);)(i=i.componentInstance._vnode)&&i.data&&(e=Un(i.data,e));for(;r(n=n.parent);)n&&n.data&&(e=Un(e,n.data));return o=e.staticClass,s=e.class,r(o)||r(s)?Fn(o,Xn(s)):"";var o,s}(n),u=i._transitionClasses;r(u)&&(a=Fn(a,Xn(u))),a!==i._prevClass&&(i.setAttribute("class",a),i._prevClass=a)}}var pr,hr={create:fr,update:fr};function vr(t,e,n){var r=pr;return function i(){var o=e.apply(null,arguments);null!==o&&_r(t,i,n,r)}}var mr=Vt&&!(Y&&Number(Y[1])<=53);function yr(t,e,n,r){if(mr){var i=un,o=e;e=o._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=i||t.timeStamp<=0||t.target.ownerDocument!==document)return o.apply(this,arguments)}}pr.addEventListener(t,e,Z?{capture:n,passive:r}:n)}function _r(t,e,n,r){(r||pr).removeEventListener(t,e._wrapper||e,n)}function gr(t,n){if(!e(t.data.on)||!e(n.data.on)){var i=n.data.on||{},o=t.data.on||{};pr=n.elm,function(t){if(r(t.__r)){var e=G?"change":"input";t[e]=[].concat(t.__r,t[e]||[]),delete t.__r}r(t.__c)&&(t.change=[].concat(t.__c,t.change||[]),delete t.__c)}(i),ie(i,o,yr,_r,vr,n.context),pr=void 0}}var br,Er={create:gr,update:gr};function xr(t,n){if(!e(t.data.domProps)||!e(n.data.domProps)){var i,o,s=n.elm,a=t.data.domProps||{},u=n.data.domProps||{};for(i in r(u.__ob__)&&(u=n.data.domProps=C({},u)),a)i in u||(s[i]="");for(i in u){if(o=u[i],"textContent"===i||"innerHTML"===i){if(n.children&&(n.children.length=0),o===a[i])continue;1===s.childNodes.length&&s.removeChild(s.childNodes[0])}if("value"===i&&"PROGRESS"!==s.tagName){s._value=o;var c=e(o)?"":String(o);wr(s,c)&&(s.value=c)}else if("innerHTML"===i&&Vn(s.tagName)&&e(s.innerHTML)){(br=br||document.createElement("div")).innerHTML="";for(var l=br.firstChild;s.firstChild;)s.removeChild(s.firstChild);for(;l.firstChild;)s.appendChild(l.firstChild)}else if(o!==a[i])try{s[i]=o}catch(t){}}}}function wr(t,e){return!t.composing&&("OPTION"===t.tagName||function(t,e){var n=!0;try{n=document.activeElement!==t}catch(t){}return n&&t.value!==e}(t,e)||function(t,e){var n=t.value,i=t._vModifiers;if(r(i)){if(i.number)return f(n)!==f(e);if(i.trim)return n.trim()!==e.trim()}return n!==e}(t,e))}var Tr={create:xr,update:xr},Or=_((function(t){var e={},n=/:(.+)/;return t.split(/;(?![^(]*\))/g).forEach((function(t){if(t){var r=t.split(n);r.length>1&&(e[r[0].trim()]=r[1].trim())}})),e}));function Cr(t){var e=Sr(t.style);return t.staticStyle?C(t.staticStyle,e):e}function Sr(t){return Array.isArray(t)?S(t):"string"==typeof t?Or(t):t}var kr,Ar=/^--/,$r=/\s*!important$/,Nr=function(t,e,n){if(Ar.test(e))t.style.setProperty(e,n);else if($r.test(n))t.style.setProperty(w(e),n.replace($r,""),"important");else{var r=Pr(e);if(Array.isArray(n))for(var i=0,o=n.length;i-1?e.split(Dr).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" "+(t.getAttribute("class")||"")+" ";n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function jr(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(Dr).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{for(var n=" "+(t.getAttribute("class")||"")+" ",r=" "+e+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?t.setAttribute("class",n):t.removeAttribute("class")}}function Ur(t){if(t){if("object"==typeof t){var e={};return!1!==t.css&&C(e,Fr(t.name||"v")),C(e,t),e}return"string"==typeof t?Fr(t):void 0}}var Fr=_((function(t){return{enterClass:t+"-enter",enterToClass:t+"-enter-to",enterActiveClass:t+"-enter-active",leaveClass:t+"-leave",leaveToClass:t+"-leave-to",leaveActiveClass:t+"-leave-active"}})),Xr=H&&!K,Hr="transition",zr="animation",Vr="transition",Br="transitionend",Gr="animation",Kr="animationend";Xr&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Vr="WebkitTransition",Br="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Gr="WebkitAnimation",Kr="webkitAnimationEnd"));var Wr=H?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function qr(t){Wr((function(){Wr(t)}))}function Yr(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),Mr(t,e))}function Jr(t,e){t._transitionClasses&&v(t._transitionClasses,e),jr(t,e)}function Zr(t,e,n){var r=ti(t,e),i=r.type,o=r.timeout,s=r.propCount;if(!i)return n();var a=i===Hr?Br:Kr,u=0,c=function(){t.removeEventListener(a,l),n()},l=function(e){e.target===t&&++u>=s&&c()};setTimeout((function(){u0&&(n=Hr,l=s,d=o.length):e===zr?c>0&&(n=zr,l=c,d=u.length):d=(n=(l=Math.max(s,c))>0?s>c?Hr:zr:null)?n===Hr?o.length:u.length:0,{type:n,timeout:l,propCount:d,hasTransform:n===Hr&&Qr.test(r[Vr+"Property"])}}function ei(t,e){for(;t.length1}function ai(t,e){!0!==e.data.show&&ri(e)}var ui=function(t){var n,s,a={},u=t.modules,c=t.nodeOps;for(n=0;nh?g(t,e(i[y+1])?null:i[y+1].elm,i,p,y,o):p>y&&E(n,f,h)}(f,v,y,o,l):r(y)?(r(t.text)&&c.setTextContent(f,""),g(f,null,y,0,y.length-1,o)):r(v)?E(v,0,v.length-1):r(t.text)&&c.setTextContent(f,""):t.text!==n.text&&c.setTextContent(f,n.text),r(h)&&r(p=h.hook)&&r(p=p.postpatch)&&p(t,n)}}}function O(t,e,n){if(i(n)&&r(t.parent))t.parent.data.pendingInsert=e;else for(var o=0;o-1,s.selected!==o&&(s.selected=o);else if(N(pi(s),r))return void(t.selectedIndex!==a&&(t.selectedIndex=a));i||(t.selectedIndex=-1)}}function fi(t,e){return e.every((function(e){return!N(e,t)}))}function pi(t){return"_value"in t?t._value:t.value}function hi(t){t.target.composing=!0}function vi(t){t.target.composing&&(t.target.composing=!1,mi(t.target,"input"))}function mi(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function yi(t){return!t.componentInstance||t.data&&t.data.transition?t:yi(t.componentInstance._vnode)}var _i={model:ci,show:{bind:function(t,e,n){var r=e.value,i=(n=yi(n)).data&&n.data.transition,o=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&i?(n.data.show=!0,ri(n,(function(){t.style.display=o}))):t.style.display=r?o:"none"},update:function(t,e,n){var r=e.value;!r!=!e.oldValue&&((n=yi(n)).data&&n.data.transition?(n.data.show=!0,r?ri(n,(function(){t.style.display=t.__vOriginalDisplay})):ii(n,(function(){t.style.display="none"}))):t.style.display=r?t.__vOriginalDisplay:"none")},unbind:function(t,e,n,r,i){i||(t.style.display=t.__vOriginalDisplay)}}},gi={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function bi(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?bi(Ve(e.children)):t}function Ei(t){var e={},n=t.$options;for(var r in n.propsData)e[r]=t[r];var i=n._parentListeners;for(var o in i)e[b(o)]=i[o];return e}function xi(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}var wi=function(t){return t.tag||ze(t)},Ti=function(t){return"show"===t.name},Oi={name:"transition",props:gi,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(wi)).length){var r=this.mode,i=n[0];if(function(t){for(;t=t.parent;)if(t.data.transition)return!0}(this.$vnode))return i;var s=bi(i);if(!s)return i;if(this._leaving)return xi(t,i);var a="__transition-"+this._uid+"-";s.key=null==s.key?s.isComment?a+"comment":a+s.tag:o(s.key)?0===String(s.key).indexOf(a)?s.key:a+s.key:s.key;var u=(s.data||(s.data={})).transition=Ei(this),c=this._vnode,l=bi(c);if(s.data.directives&&s.data.directives.some(Ti)&&(s.data.show=!0),l&&l.data&&!function(t,e){return e.key===t.key&&e.tag===t.tag}(s,l)&&!ze(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var d=l.data.transition=C({},u);if("out-in"===r)return this._leaving=!0,oe(d,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),xi(t,i);if("in-out"===r){if(ze(s))return c;var f,p=function(){f()};oe(u,"afterEnter",p),oe(u,"enterCancelled",p),oe(d,"delayLeave",(function(t){f=t}))}}return i}}},Ci=C({tag:String,moveClass:String},gi);function Si(t){t.elm._moveCb&&t.elm._moveCb(),t.elm._enterCb&&t.elm._enterCb()}function ki(t){t.data.newPos=t.elm.getBoundingClientRect()}function Ai(t){var e=t.data.pos,n=t.data.newPos,r=e.left-n.left,i=e.top-n.top;if(r||i){t.data.moved=!0;var o=t.elm.style;o.transform=o.WebkitTransform="translate("+r+"px,"+i+"px)",o.transitionDuration="0s"}}delete Ci.mode;var $i={Transition:Oi,TransitionGroup:{props:Ci,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var i=Ye(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,i(),e.call(t,n,r)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],o=this.children=[],s=Ei(this),a=0;a-1?Gn[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:Gn[t]=/HTMLUnknownElement/.test(e.toString())},C(wn.options.directives,_i),C(wn.options.components,$i),wn.prototype.__patch__=H?ui:k,wn.prototype.$mount=function(t,e){return function(t,e,n){var r;return t.$el=e,t.$options.render||(t.$options.render=pt),tn(t,"beforeMount"),r=function(){t._update(t._render(),n)},new pn(t,r,k,{before:function(){t._isMounted&&!t._isDestroyed&&tn(t,"beforeUpdate")}},!0),n=!1,null==t.$vnode&&(t._isMounted=!0,tn(t,"mounted")),t}(this,t=t&&H?function(t){return"string"==typeof t?document.querySelector(t)||document.createElement("div"):t}(t):void 0,e)},H&&setTimeout((function(){M.devtools&&et&&et.emit("init",wn)}),0);const Ni=wn;var Li=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"menuv",class:[{hide:!t.show||!t.menu},t.position,t.size,t.theme],attrs:{id:"menuv","data-uuid":t.uuid}},[n("v-style",[t._v("\n html,\n body {\n color: "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+";\n }\n\n .menuv."+t._s(t.theme)+' .menuv-header {\n background: url("https://nui-img/'+t._s(t.dictionary)+"/"+t._s(t.texture)+'") no-repeat;\n background-size: 100%;\n }\n\n .menuv.'+t._s(t.theme)+" .menuv-header .menuv-bg-icon i,\n .menuv."+t._s(t.theme)+" .menuv-header .menuv-bg-icon svg {\n color: rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n }\n\n .menuv."+t._s(t.theme)+" .menuv-subheader {\n background-color: rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items .menuv-item.active {\n border-left: 0.5em solid rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n border-right: 0.5em solid rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n background-color: rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n color: "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+";\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items .menuv-item.active i,\n .menuv."+t._s(t.theme)+" .menuv-items .menuv-item.active svg {\n color: "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+";\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items .menuv-item.active span.menuv-icon {\n border-right: 1px solid "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+";\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items span.menuv-options span.menuv-btn {\n color: "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+";\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items span.menuv-options span.menuv-btn.active {\n background-color: rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n color: "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+";\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items .menuv-item.active span.menuv-options span.menuv-btn {\n background-color: rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n color: "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+";\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items .menuv-item.active span.menuv-options span.menuv-btn.active {\n background-color: black;\n color: white;\n }\n\n .menuv."+t._s(t.theme)+' .menuv-items input[type="range"]::-webkit-slider-runnable-track {\n background: rgba('+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+", 0.50);\n box-shadow: 0px 0px 0px "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b,.5))+";\n border: 0px solid "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b,.5))+";\n }\n\n .menuv."+t._s(t.theme)+' .menuv-items input[type="range"]::-webkit-slider-thumb {\n border: 1px solid rgb('+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n background: rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n box-shadow: 0px 0px 0px "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b,.5))+";\n }\n\n .menuv."+t._s(t.theme)+' .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-thumb {\n background: '+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b))+" !important;\n border: 1px solid "+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b,.5))+" !important;\n }\n\n .menuv."+t._s(t.theme)+' .menuv-items .menuv-item.active input[type="range"]::-webkit-slider-runnable-track,\n .menuv.'+t._s(t.theme)+' .menuv-items .menuv-item.active input[type="range"]:focus::-webkit-slider-runnable-track {\n background: '+t._s(t.TEXT_COLOR(t.color.r,t.color.g,t.color.b,.5))+" !important;\n }\n\n .menuv."+t._s(t.theme)+' .menuv-items input[type="range"]:focus::-webkit-slider-runnable-track {\n background: rgba('+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+", 0.50);\n }\n\n .menuv."+t._s(t.theme)+" .menuv-items .menuv-desc {\n border-left: 0.375em solid rgb("+t._s(t.color.r)+","+t._s(t.color.g)+","+t._s(t.color.b)+");\n }\n ")]),t._v(" "),n("header",{staticClass:"menuv-header"},[n("strong",{domProps:{innerHTML:t._s(t.FORMAT_TEXT(t.title))}})]),t._v(" "),n("nav",{staticClass:"menuv-subheader",domProps:{innerHTML:t._s(t.FORMAT_TEXT(t.subtitle))}}),t._v(" "),n("ul",{ref:"items",staticClass:"menuv-items"},t._l(t.items,(function(e){return n("li",{key:e.uuid,staticClass:"menuv-item media",class:[{active:t.index+1==e.index,hasIcon:"none"!=t.ENSURE(e.icon,"none"),disabled:e.disabled},"menuv-"+e.type],attrs:{index:e.index-1}},["none"!=t.ENSURE(e.icon,"none")?n("div",{staticClass:"media-left item-icon"},[n("span",{staticClass:"menuv-icon"},[t._v(t._s(t.ENSURE(e.icon,"none")))])]):t._e(),t._v(" "),n("div",{staticClass:"media-content flex-left item-title",domProps:{innerHTML:t._s(t.FORMAT_TEXT(e.label))}}),t._v(" "),n("div",{staticClass:"media-right"},["menu"==e.type?n("i",{staticClass:"fas fa-arrow-right"}):t._e(),t._v(" "),"checkbox"==e.type?n("i",{class:{"fas fa-check":e.value,"far fa-square":!e.value}}):t._e(),t._v(" "),"range"==e.type?n("input",{attrs:{type:"range",min:e.min,max:e.max},domProps:{value:e.value}}):t._e(),t._v(" "),"confirm"==e.type?n("span",{staticClass:"menuv-options"},[n("span",{staticClass:"menuv-btn",class:{active:e.value}},[t._v("YES")]),t._v(" "),n("span",{staticClass:"menuv-btn",class:{active:!e.value}},[t._v("NO")])]):t._e(),t._v(" "),"label"==e.type?n("span",{staticClass:"menuv-label",domProps:{innerHTML:t._s(t.FORMAT_TEXT(e.value))}}):t._e(),t._v(" "),"slider"==e.type?n("span",{staticClass:"menuv-options"},[n("i",{staticClass:"fas fa-chevron-left"}),t._v(" "),n("span",{domProps:{innerHTML:t._s(t.GET_SLIDER_LABEL({uuid:e.uuid}))}}),t._v(" "),n("i",{staticClass:"fas fa-chevron-right"})]):t._e()])])})),0),t._v(" "),n("footer",{staticClass:"menuv-description",class:{hide:t.IS_DEFAULT(t.GET_CURRENT_DESCRIPTION())}},[n("strong",{domProps:{innerHTML:t._s(t.GET_CURRENT_DESCRIPTION())}})])],1)};Li._withStripped=!0;const Pi=Ni.component("v-style",{render:function(t){return t("style",this.$slots.default)}});var Ri=n(81),Ii=n.n(Ri);Ni.use(Ii(),{container:"ul.menuv-items",duration:500,easing:"ease-in",offset:-25,force:!0,cancelable:!1,onStart:!1,onDone:!1,onCancel:!1,x:!1,y:!0});var Di=function(t,e,n,r,i,o,s,a){var u,c="function"==typeof t?t.options:t;if(e&&(c.render=e,c.staticRenderFns=[],c._compiled=!0),u)if(c.functional){c._injectStyles=u;var l=c.render;c.render=function(t,e){return u.call(e),l(t,e)}}else{var d=c.beforeCreate;c.beforeCreate=d?[].concat(d,u):[u]}return{exports:t,options:c}}(Ni.extend({template:"#menuv_template",name:"menuv",components:{STYLE:Pi},data:()=>({theme:"default",resource:"menuv",uuid:"",menu:!1,show:!1,title:"MenuV",subtitle:"",position:"topleft",size:"size-110",texture:"none",dictionary:"none",color:{r:0,g:0,b:255},items:[],listener:t=>{},index:0,sounds:{},cached_indexes:{}}),destroyed(){window.removeEventListener("message",this.listener)},mounted(){this.listener=t=>{const e=t.data||t.detail;if(!e||!e.action)return;const n=e.action;this[n]&&this[n](e)},window.addEventListener("message",this.listener),this.POST("https://menuv/loaded",{})},watch:{theme(){},title(){},subtitle(){},position(){},color(){},options(){},menu(){},show(){},size(){},index(t,e){let n=null,r=null;e>=0&&e=0&&t=t.length||this.index<0)return;let n=null;const r=t[this.index];if(null!=r){for(var i=0;it.index>e.index?1:t.indext.index>e.index?1:t.index=i.length?i.push(t):i.splice(e,0,t),this.items=i.sort(((t,e)=>t.index>e.index?1:t.indext.index>e.index?1:t.index=n.length?this.FORMAT_TEXT(n[0].label||"Unknown"):this.FORMAT_TEXT(n[t].label||"Unknown")}return""},GET_CURRENT_DESCRIPTION(){const t=this.index||0;return t>=0&&t1&&(r=1),r<0&&(r=0),(.299*t+.587*e+.114*n)/255>.5?`rgba(0, 0, 0, ${r})`:`rgba(255, 255, 255, ${r})`},IS_DEFAULT:function(t){return"string"==typeof t?null==t||"nil"==t||""==t:("number"==typeof t||"boolean"==typeof t)&&0==t},KEY_PRESSED({key:t}){if(!this.menu||!this.show)return;if(void 0===t||null==t)return;const e=`KEY_${t}`;this[e]&&this[e]()},RESOURCE_STOPPED({resource:t}){this.menu&&this.resource==t&&this.RESET_MENU()},KEY_UP:function(){const t=this.PREV_INDEX(this.index);this.index!=t&&(this.index=t,this.sounds.UP&&"native"==this.sounds.UP.type&&this.POST("https://menuv/sound",{key:"UP"}))},KEY_DOWN:function(){const t=this.NEXT_INDEX(this.index);this.index!=t&&(this.index=t,this.sounds.DOWN&&"native"==this.sounds.DOWN.type&&this.POST("https://menuv/sound",{key:"DOWN"}))},KEY_LEFT:function(){if(this.index<0||this.items.length<=this.index||this.items[this.index].disabled)return;const t=this.items[this.index];if("button"!=t.type&&"menu"!=t.type&&"label"!=t.type&&"unknown"!=t.type)switch(t.type){case"confirm":case"checkbox":const e=t.value;this.items[this.index].value=!e,this.sounds.LEFT&&"native"==this.sounds.LEFT.type&&this.POST("https://menuv/sound",{key:"LEFT"});break;case"range":let n=null,r=t.value;n=r-1<=t.min?t.min:r-1>=t.max?t.max:this.items[this.index].value-1,n!=this.items[this.index].value&&(this.items[this.index].value=n,this.sounds.LEFT&&"native"==this.sounds.LEFT.type&&this.POST("https://menuv/sound",{key:"LEFT"}));break;case"slider":let i=null;const o=t.value,s=t.values||[];if(s.length<=0)return;i=o-1<0||o-1>=s.length?s.length-1:this.items[this.index].value-1,i!=this.items[this.index].value&&(this.items[this.index].value=i,this.sounds.LEFT&&"native"==this.sounds.LEFT.type&&this.POST("https://menuv/sound",{key:"LEFT"}))}},KEY_RIGHT:function(){if(this.index<0||this.items.length<=this.index||this.items[this.index].disabled)return;const t=this.items[this.index];if("button"!=t.type&&"menu"!=t.type&&"label"!=t.type&&"unknown"!=t.type)switch(t.type){case"confirm":case"checkbox":const e=t.value;this.items[this.index].value=!e,this.sounds.RIGHT&&"native"==this.sounds.RIGHT.type&&this.POST("https://menuv/sound",{key:"RIGHT"});break;case"range":let n=null,r=t.value;n=r+1<=t.min?t.min:r+1>=t.max?t.max:this.items[this.index].value+1,n!=this.items[this.index].value&&(this.items[this.index].value=n,this.sounds.RIGHT&&"native"==this.sounds.RIGHT.type&&this.POST("https://menuv/sound",{key:"RIGHT"}));break;case"slider":let i=null;const o=t.value,s=t.values||[];if(s.length<=0)return;i=o+1<0||o+1>=s.length?0:this.items[this.index].value+1,i!=this.items[this.index].value&&(this.items[this.index].value=i,this.sounds.RIGHT&&"native"==this.sounds.RIGHT.type&&this.POST("https://menuv/sound",{key:"RIGHT"}))}},KEY_ENTER:function(){if(this.index<0||this.items.length<=this.index||this.items[this.index].disabled)return;this.sounds.ENTER&&"native"==this.sounds.ENTER.type&&this.POST("https://menuv/sound",{key:"ENTER"});const t=this.items[this.index];switch(t.type){case"button":case"menu":this.POST("https://menuv/submit",{uuid:t.uuid,value:null,r:this.resource});break;case"confirm":this.POST("https://menuv/submit",{uuid:t.uuid,value:t.value,r:this.resource});break;case"range":let e=t.value;e<=t.min?e=t.min:e>=t.max&&(e=t.max),this.POST("https://menuv/submit",{uuid:t.uuid,value:e,r:this.resource});break;case"checkbox":const n=t.value;this.items[this.index].value=!n,this.POST("https://menuv/submit",{uuid:t.uuid,value:this.items[this.index].value,r:this.resource});break;case"slider":let r=t.value;const i=t.values||[];if(i.length<=0||r<0||r>=i.length)return;this.POST("https://menuv/submit",{uuid:t.uuid,value:r,r:this.resource})}},KEY_CLOSE:function(){this.sounds.CLOSE&&"native"==this.sounds.CLOSE.type&&this.POST("https://menuv/sound",{key:"CLOSE"}),this.POST("https://menuv/close",{uuid:this.uuid,r:this.resource}),this.CLOSE_MENU({uuid:this.uuid})},KEY_CLOSE_ALL:function(){this.sounds.CLOSE&&"native"==this.sounds.CLOSE.type&&this.POST("https://menuv/sound",{key:"CLOSE"}),this.POST("https://menuv/close_all",{r:this.resource}),this.RESET_MENU()},POST:function(t,e){var n=new XMLHttpRequest;n.open("POST",t,!0),n.open("POST",t,!0),n.setRequestHeader("Content-Type","application/json; charset=UTF-8"),n.send(JSON.stringify(e))},NEXT_INDEX:function(t){null!=t&&void 0!==t||(t=this.index);let e=0,n=-2;if(this.items.length<=0)return-1;for(;n<-1;)if(t+1+e=this.items.length)return-1;{const r=t+1+e-this.items.length;r=0)this.items[t-1-e].disabled?e++:n=t-1-e;else{if(e>=this.items.length)return-1;{const r=t-1-e+this.items.length;r=0?this.items[r].disabled?e++:n=r:e++}}return n<0?-1:n},NL2BR:function(t,e,n){var r=n?" ":" ";return(t+"").replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g,e?"$1"+r:"$1"+r+"$2")},FORMAT_TEXT:function(t){return t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=this.ENSURE(t,"")).replace(/\^0/g,'')).replace(/\^1/g,'')).replace(/\^2/g,'')).replace(/\^3/g,'')).replace(/\^4/g,'')).replace(/\^5/g,'')).replace(/\^6/g,'')).replace(/\^7/g,'')).replace(/\^8/g,'')).replace(/\^9/g,'')).replace(/~r~/g,'')).replace(/~g~/g,'')).replace(/~b~/g,'')).replace(/~y~/g,'')).replace(/~p~/g,'')).replace(/~c~/g,'')).replace(/~m~/g,'')).replace(/~u~/g,'')).replace(/~o~/g,'')).replace(/~n~/g," ")).replace(/~s~/g,'')).replace(/~h~/g,""),(new DOMParser).parseFromString(t||"","text/html").body.innerHTML}}}),Li);Di.options.__file="source/app/vue_templates/menuv.vue";const Mi=Di.exports;new Ni({el:"#menuv",render:t=>t(Mi)})})()})();
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/dist/assets/js/menuv.js.LICENSE.txt b/resources/[standalone]/menuv/dist/assets/js/menuv.js.LICENSE.txt
new file mode 100644
index 0000000..f9b3aa0
--- /dev/null
+++ b/resources/[standalone]/menuv/dist/assets/js/menuv.js.LICENSE.txt
@@ -0,0 +1,11 @@
+/*!
+ * vue-scrollto v2.20.0
+ * (c) 2019 Randjelovic Igor
+ * @license MIT
+ */
+
+/*!
+ * Vue.js v2.6.12
+ * (c) 2014-2020 Evan You
+ * Released under the MIT License.
+ */
diff --git a/resources/[standalone]/menuv/dist/menuv.html b/resources/[standalone]/menuv/dist/menuv.html
new file mode 100644
index 0000000..ac65934
--- /dev/null
+++ b/resources/[standalone]/menuv/dist/menuv.html
@@ -0,0 +1 @@
+MenuV
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/fxmanifest.lua b/resources/[standalone]/menuv/fxmanifest.lua
new file mode 100644
index 0000000..8063b95
--- /dev/null
+++ b/resources/[standalone]/menuv/fxmanifest.lua
@@ -0,0 +1,37 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+fx_version 'cerulean'
+game 'gta5'
+lua54 'yes'
+
+name 'MenuV'
+version '1.4.1'
+description 'FiveM menu library for creating menu\'s'
+author 'ThymonA'
+contact 'contact@arens.io'
+url 'https://github.com/ThymonA/menuv/'
+
+files {
+ 'menuv.lua',
+ 'menuv/components/*.lua',
+ 'dist/*.html',
+ 'dist/assets/css/*.css',
+ 'dist/assets/js/*.js',
+ 'dist/assets/fonts/*.woff',
+ 'languages/*.json'
+}
+
+ui_page 'dist/menuv.html'
+
+client_scripts {
+ 'config.lua',
+ 'menuv/components/utilities.lua',
+ 'menuv/menuv.lua'
+}
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/languages/en.json b/resources/[standalone]/menuv/languages/en.json
new file mode 100644
index 0000000..99dbcad
--- /dev/null
+++ b/resources/[standalone]/menuv/languages/en.json
@@ -0,0 +1,22 @@
+{
+ "language": "en",
+ "translators": [
+ {
+ "name": "ThymonA",
+ "emails": ["contact@thymonarens.nl"],
+ "github": "https://github.com/ThymonA"
+ }
+ ],
+ "translations": {
+ "keyboard": "Keyboard",
+ "controller": "Controller",
+ "keybind_key_up": "Up in a menu",
+ "keybind_key_down": "Down in a menu",
+ "keybind_key_left": "Left in a menu",
+ "keybind_key_right": "Right in a menu",
+ "keybind_key_enter": "Confirm item in menu",
+ "keybind_key_close": "Closing a menu",
+ "keybind_key_close_all": "Close all menus (including parents)",
+ "open_menu": "Open menu '%s' with this key"
+ }
+}
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/languages/fr.json b/resources/[standalone]/menuv/languages/fr.json
new file mode 100644
index 0000000..1a91fb5
--- /dev/null
+++ b/resources/[standalone]/menuv/languages/fr.json
@@ -0,0 +1,22 @@
+{
+ "language": "fr",
+ "translators": [
+ {
+ "name": "Korioz",
+ "emails": ["pro.korioz@protonmail.com"],
+ "github": "https://github.com/Korioz"
+ }
+ ],
+ "translations": {
+ "keyboard": "Clavier",
+ "controller": "Manette",
+ "keybind_key_up": "Haut dans un menu",
+ "keybind_key_down": "Bas dans un menu",
+ "keybind_key_left": "Gauche dans un menu",
+ "keybind_key_right": "Droite dans un menu",
+ "keybind_key_enter": "Confirmer dans un menu",
+ "keybind_key_close": "Fermer un menu",
+ "keybind_key_close_all": "Fermer tout les menus",
+ "open_menu": "Ouvrir menu '%s'"
+ }
+}
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/languages/nl.json b/resources/[standalone]/menuv/languages/nl.json
new file mode 100644
index 0000000..9b8333b
--- /dev/null
+++ b/resources/[standalone]/menuv/languages/nl.json
@@ -0,0 +1,22 @@
+{
+ "language": "nl",
+ "translators": [
+ {
+ "name": "ThymonA",
+ "emails": ["contact@thymonarens.nl"],
+ "github": "https://github.com/ThymonA"
+ }
+ ],
+ "translations": {
+ "keyboard": "Toetsenbord",
+ "controller": "Controller",
+ "keybind_key_up": "Omhoog in een menu",
+ "keybind_key_down": "Omlaag in een menu",
+ "keybind_key_left": "Links in een menu",
+ "keybind_key_right": "Rechts in een menu",
+ "keybind_key_enter": "Item bevestigen in menu",
+ "keybind_key_close": "Sluiten van een menu",
+ "keybind_key_close_all": "Sluit alle menu's (inclusief ouders)",
+ "open_menu": "Menu '%s' openen met deze key"
+ }
+}
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/menuv.lua b/resources/[standalone]/menuv/menuv.lua
new file mode 100644
index 0000000..25799c8
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv.lua
@@ -0,0 +1,2486 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+local pairs = assert(pairs)
+local rawget = assert(rawget)
+local rawset = assert(rawset)
+local insert = assert(table.insert)
+local remove = assert(table.remove)
+local format = assert(string.format)
+local upper = assert(string.upper)
+local lower = assert(string.lower)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
+local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded)
+local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict)
+local REGISTER_KEY_MAPPING = assert(RegisterKeyMapping)
+local REGISTER_COMMAND = assert(RegisterCommand)
+local GET_HASH_KEY = assert(GetHashKey)
+local CreateThread = assert(Citizen.CreateThread)
+local Wait = assert(Citizen.Wait)
+
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+Config = {
+ Language = 'en',
+ HideInterval = 250,
+ Sounds = {
+ UP = {
+ type = 'native',
+ name = 'NAV_UP_DOWN',
+ library = 'HUD_FREEMODE_SOUNDSET'
+ },
+ DOWN = {
+ type = 'native',
+ name = 'NAV_UP_DOWN',
+ library = 'HUD_FREEMODE_SOUNDSET'
+ },
+ LEFT = {
+ type = 'native',
+ name = 'NAV_LEFT_RIGHT',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ },
+ RIGHT = {
+ type = 'native',
+ name = 'NAV_LEFT_RIGHT',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ },
+ ENTER = {
+ type = 'native',
+ name = 'SELECT',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ },
+ CLOSE = {
+ type = 'native',
+ name = 'BACK',
+ library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
+ }
+ }
+}
+
+_G.Config = Config
+_ENV.Config = Config
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+local type = assert(type)
+local tonumber = assert(tonumber)
+local tostring = assert(tostring)
+local lower = assert(string.lower)
+local upper = assert(string.upper)
+local sub = assert(string.sub)
+local encode = assert(json.encode)
+local decode = assert(json.decode)
+local floor = assert(math.floor)
+local random = assert(math.random)
+local randomseed = assert(math.randomseed)
+local rawget = assert(rawget)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local GET_GAME_TIMER = assert(GetGameTimer)
+local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
+
+--- Utilities for MenuV
+---@class Utilities
+local Utilities = setmetatable({ __class = 'Utilities' }, {})
+
+--- Returns `true` if `input` starts with `start`, otherwise `false`
+---@param input string Checks if this string starts with `start`
+---@param start string Checks if `input` starts with this
+---@return boolean `true` if `input` starts with `start`, otherwise `false`
+function Utilities:StartsWith(input, start)
+ if (self:Typeof(input) ~= 'string') then return false end
+ if (self:Typeof(start) == 'number') then start = tostring(start) end
+ if (self:Typeof(start) ~= 'string') then return false end
+
+ return sub(input, 1, #start) == start
+end
+
+--- Returns `true` if `input` ends with `ends`, otherwise `false`
+---@param input string Checks if this string ends with `ends`
+---@param ends string Checks if `input` ends with this
+---@return boolean `true` if `input` ends with `ends`, otherwise `false`
+function Utilities:EndsWith(input, ends)
+ if (self:Typeof(input) ~= 'string') then return false end
+ if (self:Typeof(ends) == 'number') then ends = tostring(ends) end
+ if (self:Typeof(ends) ~= 'string') then return false end
+
+ return sub(input, -#ends) == ends
+end
+
+--- Returns the type of given `input`
+---@param input any Any input
+---@return string Type of given input
+function Utilities:Typeof(input)
+ if (input == nil) then return 'nil' end
+
+ local rawType = type(input) or 'nil'
+
+ if (rawType ~= 'table') then return rawType end
+
+ local isFXFunction = rawget(input, '__cfx_functionReference') ~= nil or
+ rawget(input, '__cfx_async_retval') ~= nil
+
+ if (isFXFunction) then return 'function' end
+ if (rawget(input, '__cfx_functionSource') ~= nil) then return 'number' end
+
+ local rawClass = rawget(input, '__class')
+
+ if (rawClass ~= nil) then return type(rawClass) == 'string' and rawClass or 'class' end
+
+ local rawTableType = rawget(input, '__type')
+
+ if (rawTableType ~= nil) then return type(rawTableType) == 'string' and rawTableType or 'table' end
+
+ return rawType
+end
+
+local INPUT_GROUPS = {
+ [0] = "KEYBOARD",
+ [2] = "CONTROLLER"
+}
+
+local INPUT_TYPE_GROUPS = {
+ ["KEYBOARD"] = 0,
+ ["MOUSE_ABSOLUTEAXIS"] = 0,
+ ["MOUSE_CENTEREDAXIS"] = 0,
+ ["MOUSE_RELATIVEAXIS"] = 0,
+ ["MOUSE_SCALEDAXIS"] = 0,
+ ["MOUSE_NORMALIZED"] = 0,
+ ["MOUSE_WHEEL"] = 0,
+ ["MOUSE_BUTTON"] = 0,
+ ["MOUSE_BUTTONANY"] = 0,
+ ["MKB_AXIS"] = 0,
+ ["PAD_AXIS"] = 2,
+ ["PAD_DIGITALBUTTON"] = 2,
+ ["PAD_DIGITALBUTTONANY"] = 2,
+ ["PAD_ANALOGBUTTON"] = 2,
+ ["JOYSTICK_POV"] = 2,
+ ["JOYSTICK_POV_AXIS"] = 2,
+ ["JOYSTICK_BUTTON"] = 2,
+ ["JOYSTICK_AXIS"] = 2,
+ ["JOYSTICK_IAXIS"] = 2,
+ ["JOYSTICK_AXIS_NEGATIVE"] = 2,
+ ["JOYSTICK_AXIS_POSITIVE"] = 2,
+ ["PAD_DEBUGBUTTON"] = 2,
+ ["GAME_CONTROLLED"] = 2,
+ ["DIGITALBUTTON_AXIS"] = 2,
+}
+
+function Utilities:GetInputTypeGroup(inputType)
+ return INPUT_TYPE_GROUPS[inputType] or 0
+end
+
+function Utilities:GetInputGroupName(inputTypeGroup)
+ return INPUT_GROUPS[inputTypeGroup] or "KEYBOARD"
+end
+
+--- Transform any `input` to the same type as `defaultValue`
+---@type function
+---@param input any Transform this `input` to `defaultValue`'s type
+---@param defaultValue any Returns this if `input` can't transformed to this type
+---@param ignoreDefault boolean Don't return default value if this is true
+---@return any Returns `input` matches the `defaultValue` type or `defaultValue`
+function Utilities:Ensure(input, defaultValue, ignoreDefault)
+ ignoreDefault = type(ignoreDefault) == 'boolean' and ignoreDefault or false
+
+ if (defaultValue == nil) then return nil end
+
+ local requiredType = self:Typeof(defaultValue)
+
+ if (requiredType == 'nil') then return nil end
+
+ local inputType = self:Typeof(input)
+
+ if (inputType == requiredType) then return input end
+ if (inputType == 'nil') then return defaultValue end
+
+ if (requiredType == 'number') then
+ if (inputType == 'boolean') then return input and 1 or 0 end
+
+ return tonumber(input) or (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'string') then
+ if (inputType == 'boolean') then return input and 'yes' or 'no' end
+ if (inputType == 'vector3') then return encode({ x = input.x, y = input.y, z = input.z }) or (not ignoreDefault and defaultValue or nil) end
+ if (inputType == 'vector2') then return encode({ x = input.x, y = input.y }) or (not ignoreDefault and defaultValue or nil) end
+ if (inputType == 'table') then return encode(input) or (not ignoreDefault and defaultValue or nil) end
+
+ local result = tostring(input)
+
+ if (result == 'nil') then
+ return not ignoreDefault and defaultValue or 'nil'
+ end
+
+ return result
+ end
+
+ if (requiredType == 'boolean') then
+ if (inputType == 'string') then
+ input = lower(input)
+
+ if (input == 'true' or input == '1' or input == 'yes' or input == 'y') then return true end
+ if (input == 'false' or input == '0' or input == 'no' or input == 'n') then return false end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (inputType == 'number') then
+ if (input == 1) then return true end
+ if (input == 0) then return false end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'table') then
+ if (inputType == 'string') then
+ if (self:StartsWith(input, '{') and self:EndsWith(input, '}')) then
+ return decode(input) or (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (self:StartsWith(input, '[') and self:EndsWith(input, ']')) then
+ return decode(input) or (not ignoreDefault and defaultValue or nil)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (inputType == 'vector3') then return { x = input.x or 0, y = input.y or 0, z = input.z or 0 } end
+ if (inputType == 'vector2') then return { x = input.x or 0, y = input.y or 0 } end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'vector3') then
+ if (inputType == 'table') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+ local _z = self:Ensure(input.z, defaultValue.z)
+
+ return vector3(_x, _y, _z)
+ end
+
+ if (inputType == 'vector2') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+
+ return vector3(_x, _y, 0)
+ end
+
+ if (inputType == 'number') then
+ return vector3(input, input, input)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'vector2') then
+ if (inputType == 'table') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+
+ return vector2(_x, _y)
+ end
+
+ if (inputType == 'vector3') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+
+ return vector2(_x, _y)
+ end
+
+ if (inputType == 'number') then
+ return vector2(input, input)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+end
+
+--- Checks if input exists in inputs
+--- '0' and 0 are both the same '0' == 0 equals `true`
+--- 'yes' and true are both the same 'yes' == true equals `true`
+---@param input any Any input
+---@param inputs any[] Any table
+---@param checkType string | "'value'" | "'key'" | "'both'"
+---@return boolean Returns `true` if input has been found as `key` and/or `value`
+function Utilities:Any(input, inputs, checkType)
+ if (input == nil) then return false end
+ if (inputs == nil) then return false end
+
+ inputs = self:Ensure(inputs, {})
+ checkType = lower(self:Ensure(checkType, 'value'))
+
+ local checkMethod = 1
+
+ if (checkType == 'value' or checkType == 'v') then
+ checkMethod = 1
+ elseif (checkType == 'key' or checkType == 'k') then
+ checkMethod = -1
+ elseif (checkType == 'both' or checkType == 'b') then
+ checkMethod = 0
+ end
+
+ for k, v in pairs(inputs) do
+ if (checkMethod == 0 or checkMethod == -1) then
+ local checkK = self:Ensure(input, k, true)
+
+ if (checkK ~= nil and checkK == k) then return true end
+ end
+
+ if (checkMethod == 0 or checkMethod == 1) then
+ local checkV = self:Ensure(input, v, true)
+
+ if (checkV ~= nil and checkV == v) then return true end
+ end
+ end
+
+ return false
+end
+
+--- Round any `value`
+---@param value number Round this value
+---@param decimal number Number of decimals
+---@return number Rounded number
+function Utilities:Round(value, decimal)
+ value = self:Ensure(value, 0)
+ decimal = self:Ensure(decimal, 0)
+
+ if (decimal > 0) then
+ return floor((value * 10 ^ decimal) + 0.5) / (10 ^ decimal)
+ end
+
+ return floor(value + 0.5)
+end
+
+--- Checks if `item1` equals `item2`
+---@param item1 any Item1
+---@param item2 any Item2
+---@return boolean `true` if both are equal, otherwise `false`
+function Utilities:Equal(item1, item2)
+ if (item1 == nil and item2 == nil) then return true end
+ if (item1 == nil or item2 == nil) then return false end
+
+ if (type(item1) == 'table') then
+ local item1EQ = rawget(item1, '__eq')
+
+ if (item1EQ ~= nil and self:Typeof(item1EQ) == 'function') then
+ return item1EQ(item1, item2)
+ end
+
+ return item1 == item2
+ end
+
+ if (type(item2) == 'table') then
+ local item2EQ = rawget(item2, '__eq')
+
+ if (item2EQ ~= nil and self:Typeof(item2EQ) == 'function') then
+ return item2EQ(item2, item1)
+ end
+
+ return item2 == item1
+ end
+
+ return item1 == item2
+end
+
+local function tohex(x)
+ x = Utilities:Ensure(x, 32)
+
+ local s, base, d = '', 16
+
+ while x > 0 do
+ d = x % base + 1
+ x = floor(x / base)
+ s = sub('0123456789abcdef', d, d) .. s
+ end
+
+ while #s < 2 do s = ('0%s'):format(s) end
+
+ return s
+end
+
+local function bitwise(x, y, matrix)
+ x = Utilities:Ensure(x, 32)
+ y = Utilities:Ensure(y, 16)
+ matrix = Utilities:Ensure(matrix, {{0,0}, {0, 1}})
+
+ local z, pow = 0, 1
+
+ while x > 0 or y > 0 do
+ z = z + (matrix[x %2 + 1][y %2 + 1] * pow)
+ pow = pow * 2
+ x = floor(x / 2)
+ y = floor(y / 2)
+ end
+
+ return z
+end
+
+--- Generates a random UUID like: 00000000-0000-0000-0000-000000000000
+---@return string Random generated UUID
+function Utilities:UUID()
+ randomseed(GET_GAME_TIMER() + random(30720, 92160))
+
+ ---@type number[]
+ local bytes = {
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255)
+ }
+
+ bytes[7] = bitwise(bytes[7], 0x0f, {{0,0},{0,1}})
+ bytes[7] = bitwise(bytes[7], 0x40, {{0,1},{1,1}})
+ bytes[9] = bitwise(bytes[7], 0x3f, {{0,0},{0,1}})
+ bytes[9] = bitwise(bytes[7], 0x80, {{0,1},{1,1}})
+
+ return upper(('%s%s%s%s-%s%s-%s%s-%s%s-%s%s%s%s%s%s'):format(
+ tohex(bytes[1]), tohex(bytes[2]), tohex(bytes[3]), tohex(bytes[4]),
+ tohex(bytes[5]), tohex(bytes[6]),
+ tohex(bytes[7]), tohex(bytes[8]),
+ tohex(bytes[9]), tohex(bytes[10]),
+ tohex(bytes[11]), tohex(bytes[12]), tohex(bytes[13]), tohex(bytes[14]), tohex(bytes[15]), tohex(bytes[16])
+ ))
+end
+
+--- Replace a string that contains `this` to `that`
+---@param str string String where to replace in
+---@param this string Word that's need to be replaced
+---@param that string Replace `this` whit given string
+---@return string String where `this` has been replaced with `that`
+function Utilities:Replace(str, this, that)
+ local b, e = str:find(this, 1, true)
+
+ if b == nil then
+ return str
+ else
+ return str:sub(1, b - 1) .. that .. self:Replace(str:sub(e + 1), this, that)
+ end
+end
+
+_G.Utilities = Utilities
+_ENV.Utilities = Utilities
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+---@type Utilities
+local U = assert(Utilities)
+local type = assert(type)
+local pairs = assert(pairs)
+local lower = assert(string.lower)
+local upper = assert(string.upper)
+local sub = assert(string.sub)
+local pack = assert(table.pack)
+local unpack = assert(table.unpack)
+local insert = assert(table.insert)
+local rawset = assert(rawset)
+local rawget = assert(rawget)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local CreateThread = assert(Citizen.CreateThread)
+
+--- Create a new menu item
+---@param info table Menu information
+---@return Item New item
+function CreateMenuItem(info)
+ info = U:Ensure(info, {})
+
+ local item = {
+ ---@type Menu|nil
+ __menu = U:Ensure(info.__Menu or info.__menu, { __class = 'Menu', __type = 'Menu' }, true) or nil,
+ ---@type string
+ __event = U:Ensure(info.PrimaryEvent or info.primaryEvent, 'unknown'),
+ ---@type string
+ UUID = U:UUID(),
+ ---@type string
+ Icon = U:Ensure(info.Icon or info.icon, 'none'),
+ ---@type string
+ Label = U:Ensure(info.Label or info.label, ''),
+ ---@type string
+ Description = U:Ensure(info.Description or info.description, ''),
+ ---@type any
+ Value = info.Value or info.value,
+ ---@type table[]
+ Values = {},
+ ---@type number
+ Min = U:Ensure(info.Min or info.min, 0),
+ ---@type number
+ Max = U:Ensure(info.Max or info.max, 0),
+ ---@type boolean
+ Disabled = U:Ensure(info.Disabled or info.disabled, false),
+ ---@type table
+ Events = U:Ensure(info.Events or info.events, {}),
+ ---@type boolean
+ SaveOnUpdate = U:Ensure(info.SaveOnUpdate or info.saveOnUpdate, false),
+ ---@param t Item
+ ---@param event string Name of Event
+ Trigger = function(t, event, ...)
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ local args = pack(...)
+
+ for _, v in pairs(t.Events[event]) do
+ CreateThread(function()
+ v(t, unpack(args))
+ end)
+ end
+ end,
+ ---@param t Item
+ ---@param event string Name of event
+ ---@param func function|Menu Function or Menu to trigger
+ On = function(t, event, func)
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ local _type = U:Typeof(func)
+
+ if (_type == 'Menu') then
+ local menu_t = {
+ __class = 'function',
+ __type = 'function',
+ func = function(t) MenuV:OpenMenu(t.uuid) end,
+ uuid = func.UUID or func.uuid or U:UUID()
+ }
+ local menu_mt = { __index = menu_t, __call = function(t) t:func() end }
+ local menu_item = setmetatable(menu_t, menu_mt)
+
+ insert(t.Events[event], menu_item)
+
+ return
+ end
+
+ func = U:Ensure(func, function() end)
+
+ insert(t.Events[event], func)
+ end,
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Validate = U:Ensure(info.Validate or info.validate, function(t, k, v)
+ return true
+ end),
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Parser = U:Ensure(info.Parser or info.parser, function(t, k, v)
+ return v
+ end),
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v)
+ end),
+ ---@param t Item
+ ---@return any
+ GetValue = function(t)
+ local itemType = U:Ensure(t.__type, 'unknown')
+
+ if (itemType == 'button' or itemType == 'menu' or itemType == 'unknown') then
+ return t.Value
+ end
+
+ if (itemType == 'checkbox' or itemType == 'confirm') then
+ return U:Ensure(t.Value, false)
+ end
+
+ if (itemType == 'slider') then
+ for _, item in pairs(t.Values) do
+ if (item.Value == t.Value) then
+ return item.Value
+ end
+ end
+
+ return nil
+ end
+
+ if (itemType == 'range') then
+ local rawValue = U:Ensure(t.Value, 0)
+
+ if (t.Min > rawValue) then
+ return t.Min
+ end
+
+ if (t.Max < rawValue) then
+ return t.Max
+ end
+
+ return rawValue
+ end
+ end,
+ ---@return Menu|nil
+ GetParentMenu = function(t)
+ return t.__menu or nil
+ end
+ }
+
+ item.Events.OnEnter = {}
+ item.Events.OnLeave = {}
+ item.Events.OnUpdate = {}
+ item.Events.OnDestroy = {}
+
+ local mt = {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ __tostring = function(t)
+ return t.UUID
+ end,
+ __call = function(t, ...)
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger(t.__event, ...)
+ end
+ end,
+ __newindex = function(t, k, v)
+ local key = U:Ensure(k, 'unknown')
+ local oldValue = rawget(t.data, k)
+ local checkInput = t.Validate ~= nil and type(t.Validate) == 'function'
+ local inputParser = t.Parser ~= nil and type(t.Parser) == 'function'
+ local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function'
+
+ if (checkInput) then
+ local result = t:Validate(key, v)
+ result = U:Ensure(result, true)
+
+ if (not result) then
+ return
+ end
+ end
+
+ if (inputParser) then
+ local parsedValue = t:Parser(key, v)
+
+ v = parsedValue or v
+ end
+
+ rawset(t.data, k, v)
+
+ if (updateIndexTrigger) then
+ t:NewIndex(key, v)
+ end
+
+ if (t.__menu ~= nil and U:Typeof(t.__menu) == 'Menu' and t.__menu.Trigger ~= nil and U:Typeof( t.__menu.Trigger) == 'function') then
+ t.__menu:Trigger('update', 'UpdateItem', t)
+ end
+
+ if (key == 'Value' and t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', key, v, oldValue)
+ end
+ end,
+ __metatable = 'MenuV'
+ }
+
+ ---@class Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field public SaveOnUpdate boolean Save on `update`
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function|Menu)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public Parser fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public GetParentMenu func(t: Item):Menu|nil
+ local i = setmetatable({ data = item, __class = 'Item', __type = U:Ensure(info.Type or info.type, 'unknown') }, mt)
+
+ for k, v in pairs(info or {}) do
+ local key = U:Ensure(k, 'unknown')
+
+ if (key == 'unknown') then return end
+
+ i:On(key, v)
+ end
+
+ return i
+end
+
+_ENV.CreateMenuItem = CreateMenuItem
+_G.CreateMenuItem = CreateMenuItem
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+---@type Utilities
+local U = assert(Utilities)
+local type = assert(type)
+local next = assert(next)
+local pairs = assert(pairs)
+local ipairs = assert(ipairs)
+local lower = assert(string.lower)
+local upper = assert(string.upper)
+local sub = assert(string.sub)
+local insert = assert(table.insert)
+local remove = assert(table.remove)
+local pack = assert(table.pack)
+local unpack = assert(table.unpack)
+local encode = assert(json.encode)
+local rawset = assert(rawset)
+local rawget = assert(rawget)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
+local GET_INVOKING_RESOURCE = assert(GetInvokingResource)
+local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded)
+local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict)
+
+--- MenuV local variable
+local current_resource = GET_CURRENT_RESOURCE_NAME()
+
+--- Returns default empty table for items
+---@returns items
+function CreateEmptyItemsTable(data)
+ data = U:Ensure(data, {})
+ data.ToTable = function(t)
+ local tempTable = {}
+ local index = 0
+
+ for _, option in pairs(t) do
+ index = index + 1
+
+ tempTable[index] = {
+ index = index,
+ type = option.__type,
+ uuid = U:Ensure(option.UUID, 'unknown'),
+ icon = U:Ensure(option.Icon, 'none'),
+ label = U:Ensure(option.Label, 'Unknown'),
+ description = U:Ensure(option.Description, ''),
+ value = 'none',
+ values = {},
+ min = U:Ensure(option.Min, 0),
+ max = U:Ensure(option.Max, 0),
+ disabled = U:Ensure(option.Disabled, false)
+ }
+
+ if (option.__type == 'button' or option.__type == 'menu') then
+ tempTable[index].value = 'none'
+ elseif (option.__type == 'checkbox' or option.__type == 'confirm') then
+ tempTable[index].value = U:Ensure(option.Value, false)
+ elseif (option.__type == 'range') then
+ tempTable[index].value = U:Ensure(option.Value, 0)
+
+ if (tempTable[index].value <= tempTable[index].min) then
+ tempTable[index].value = tempTable[index].min
+ elseif (tempTable[index].value >= tempTable[index].max) then
+ tempTable[index].value = tempTable[index].max
+ end
+ elseif (option.__type == 'slider') then
+ tempTable[index].value = 0
+ end
+
+ local _values = U:Ensure(option.Values, {})
+ local vIndex = 0
+
+ for valueIndex, value in pairs(_values) do
+ vIndex = vIndex + 1
+
+ tempTable[index].values[vIndex] = {
+ label = U:Ensure(value.Label, 'Option'),
+ description = U:Ensure(value.Description, ''),
+ value = vIndex
+ }
+
+ if (option.__type == 'slider') then
+ if (U:Ensure(option.Value, 0) == valueIndex) then
+ tempTable[index].value = (valueIndex - 1)
+ end
+ end
+ end
+ end
+
+ return tempTable
+ end
+ data.ItemToTable = function(t, i)
+ local tempTable = {}
+ local index = 0
+ local uuid = U:Typeof(i) == 'Item' and i.UUID or U:Ensure(i, '00000000-0000-0000-0000-000000000000')
+
+ for _, option in pairs(t) do
+ index = index + 1
+
+ if (option.UUID == uuid) then
+ tempTable = {
+ index = index,
+ type = option.__type,
+ uuid = U:Ensure(option.UUID, 'unknown'),
+ icon = U:Ensure(option.Icon, 'none'),
+ label = U:Ensure(option.Label, 'Unknown'),
+ description = U:Ensure(option.Description, ''),
+ value = 'none',
+ values = {},
+ min = U:Ensure(option.Min, 0),
+ max = U:Ensure(option.Max, 0),
+ disabled = U:Ensure(option.Disabled, false)
+ }
+
+ if (option.__type == 'button' or option.__type == 'menu') then
+ tempTable.value = 'none'
+ elseif (option.__type == 'checkbox' or option.__type == 'confirm') then
+ tempTable.value = U:Ensure(option.Value, false)
+ elseif (option.__type == 'range') then
+ tempTable.value = U:Ensure(option.Value, 0)
+
+ if (tempTable.value <= tempTable.min) then
+ tempTable.value = tempTable.min
+ elseif (tempTable.value >= tempTable.max) then
+ tempTable.value = tempTable.max
+ end
+ elseif (option.__type == 'slider') then
+ tempTable.value = 0
+ end
+
+ local _values = U:Ensure(option.Values, {})
+ local vIndex = 0
+
+ for valueIndex, value in pairs(_values) do
+ vIndex = vIndex + 1
+
+ tempTable.values[vIndex] = {
+ label = U:Ensure(value.Label, 'Option'),
+ description = U:Ensure(value.Description, ''),
+ value = vIndex
+ }
+
+ if (option.__type == 'slider') then
+ if (U:Ensure(option.Value, 0) == valueIndex) then
+ tempTable.value = (valueIndex - 1)
+ end
+ end
+ end
+
+ return tempTable
+ end
+ end
+
+ return tempTable
+ end
+ data.AddItem = function(t, item)
+ if (U:Typeof(item) == 'Item') then
+ local newIndex = #(U:Ensure(rawget(t, 'data'), {})) + 1
+
+ rawset(t.data, newIndex, item)
+
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', 'AddItem', item)
+ end
+ end
+
+ return U:Ensure(rawget(t, 'data'), {})
+ end
+
+ local item_pairs = function(t, k)
+ local _k, _v = next((rawget(t, 'data') or {}), k)
+
+ if (_v ~= nil and type(_v) ~= 'table') then
+ return item_pairs(t, _k)
+ end
+
+ return _k, _v
+ end
+
+ local item_ipairs = function(t, k)
+ local _k, _v = next((rawget(t, 'data') or {}), k)
+
+ if (_v ~= nil and (type(_v) ~= 'table' or type(_k) ~= 'number')) then
+ return item_ipairs(t, _k)
+ end
+
+ return _k, _v
+ end
+
+ _G.item_pairs = item_pairs
+ _ENV.item_pairs = item_pairs
+
+ ---@class items
+ return setmetatable({ data = data, Trigger = nil }, {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ __newindex = function(t, k, v)
+ local oldValue = rawget(t.data, k)
+
+ rawset(t.data, k, v)
+
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ if (oldValue == nil) then
+ t:Trigger('update', 'AddItem', v)
+ elseif (oldValue ~= nil and v == nil) then
+ t:Trigger('update', 'RemoveItem', oldValue)
+ elseif (oldValue ~= v) then
+ t:Trigger('update', 'UpdateItem', v, oldValue)
+ end
+ end
+ end,
+ __call = function(t, func)
+ rawset(t, 'Trigger', U:Ensure(func, function() end))
+ end,
+ __pairs = function(t)
+ local k = nil
+
+ return function()
+ local v
+
+ k, v = item_pairs(t, k)
+
+ return k, v
+ end, t, nil
+ end,
+ __ipairs = function(t)
+ local k = nil
+
+ return function()
+ local v
+
+ k, v = item_ipairs(t, k)
+
+ return k, v
+ end, t, 0
+ end,
+ __len = function(t)
+ local items = U:Ensure(rawget(t, 'data'), {})
+ local itemCount = 0
+
+ for _, v in pairs(items) do
+ if (U:Typeof(v) == 'Item') then
+ itemCount = itemCount + 1
+ end
+ end
+
+ return itemCount
+ end
+ })
+end
+
+--- Load a texture dictionary if not already loaded
+---@param textureDictionary string Name of texture dictionary
+local function LoadTextureDictionary(textureDictionary)
+ textureDictionary = U:Ensure(textureDictionary, 'menuv')
+
+ if (HAS_STREAMED_TEXTURE_DICT_LOADED(textureDictionary)) then return end
+
+ REQUEST_STREAMED_TEXTURE_DICT(textureDictionary, true)
+end
+
+--- Create a new menu item
+---@param info table Menu information
+---@return Menu New item
+function CreateMenu(info)
+ info = U:Ensure(info, {})
+
+ local namespace = U:Ensure(info.Namespace or info.namespace, 'unknown')
+ local namespace_available = MenuV:IsNamespaceAvailable(namespace)
+
+ if (not namespace_available) then
+ error(("[MenuV] Namespace '%s' is already taken, make sure it is unique."):format(namespace))
+ end
+
+ local theme = lower(U:Ensure(info.Theme or info.theme, 'default'))
+
+ if (theme ~= 'default' and theme ~= 'native') then
+ theme = 'default'
+ end
+
+ if (theme == 'native') then
+ info.R, info.G, info.B = 255, 255, 255
+ info.r, info.g, info.b = 255, 255, 255
+ end
+
+ local item = {
+ ---@type string
+ Namespace = namespace,
+ ---@type boolean
+ IsOpen = false,
+ ---@type string
+ UUID = U:UUID(),
+ ---@type string
+ Title = not (info.Title or info.title) and ' ' or U:Ensure(info.Title or info.title, 'MenuV'),
+ ---@type string
+ Subtitle = U:Ensure(info.Subtitle or info.subtitle, ''),
+ ---@type string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
+ Position = U:Ensure(info.Position or info.position, 'topleft'),
+ ---@type table
+ Color = {
+ R = U:Ensure(info.R or info.r, 0),
+ G = U:Ensure(info.G or info.g, 0),
+ B = U:Ensure(info.B or info.b, 255)
+ },
+ ---@type string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'"
+ Size = U:Ensure(info.Size or info.size, 'size-110'),
+ ---@type string
+ Dictionary = U:Ensure(info.Dictionary or info.dictionary, 'menuv'),
+ ---@type string
+ Texture = U:Ensure(info.Texture or info.texture, 'default'),
+ ---@type table
+ Events = U:Ensure(info.Events or info.events, {}),
+ ---@type string
+ Theme = theme,
+ ---@type Item[]
+ Items = CreateEmptyItemsTable({}),
+ ---@param t Menu
+ ---@param event string Name of Event
+ Trigger = function(t, event, ...)
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ if (event == 'OnOpen') then rawset(t, 'IsOpen', true)
+ elseif (event == 'OnClose') then rawset(t, 'IsOpen', false) end
+
+ local args = pack(...)
+
+ for _, v in pairs(t.Events[event]) do
+ if (type(v) == 'table' and U:Typeof(v.func) == 'function') then
+ CreateThread(function()
+ if (event == 'OnClose') then
+ v.func(t, unpack(args))
+ else
+ local threadId = coroutine.running()
+
+ if (threadId ~= nil) then
+ insert(t.data.Threads, threadId)
+
+ v.func(t, unpack(args))
+
+ for i = 0, #(t.data.Threads or {}), 1 do
+ if (t.data.Threads[i] == threadId) then
+ remove(t.data.Threads, i)
+ return
+ end
+ end
+ end
+ end
+ end)
+ end
+ end
+ end,
+ ---@type thread[]
+ Threads = {},
+ ---@param t Menu
+ DestroyThreads = function(t)
+ for _, threadId in pairs(t.data.Threads or {}) do
+ local threadStatus = coroutine.status(threadId)
+
+ if (threadStatus ~= nil and threadStatus ~= 'dead') then
+ coroutine.close(threadId)
+ end
+ end
+
+ t.data.Threads = {}
+ end,
+ ---@param t Menu
+ ---@param event string Name of event
+ ---@param func function|Menu Function or Menu to trigger
+ ---@return string UUID of event
+ On = function(t, event, func)
+ local ir = GET_INVOKING_RESOURCE()
+ local resource = U:Ensure(ir, current_resource)
+
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ func = U:Ensure(func, function() end)
+
+ local uuid = U:UUID()
+
+ insert(t.Events[event], {
+ __uuid = uuid,
+ __resource = resource,
+ func = func
+ })
+
+ return uuid
+ end,
+ ---@param t Menu
+ ---@param event string Name of event
+ ---@param uuid string UUID of event
+ RemoveOnEvent = function(t, event, uuid)
+ local ir = GET_INVOKING_RESOURCE()
+ local resource = U:Ensure(ir, current_resource)
+
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ uuid = U:Ensure(uuid, '00000000-0000-0000-0000-000000000000')
+
+ for i = 1, #t.Events[event], 1 do
+ if (t.Events[event][i] ~= nil and
+ t.Events[event][i].__uuid == uuid and
+ t.Events[event][i].__resource == resource) then
+ remove(t.Events[event], i)
+ end
+ end
+ end,
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Validate = U:Ensure(info.Validate or info.validate, function(t, k, v)
+ return true
+ end),
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Parser = function(t, k, v)
+ if (k == 'Position' or k == 'position') then
+ local position = lower(U:Ensure(v, 'topleft'))
+
+ if (U:Any(position, {'topleft', 'topcenter', 'topright', 'centerleft', 'center', 'centerright', 'bottomleft', 'bottomcenter', 'bottomright'}, 'value')) then
+ return position
+ else
+ return 'topleft'
+ end
+ end
+
+ return v
+ end,
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v)
+ end),
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about button
+ ---@return Item New item
+ AddButton = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'button'
+ info.Events = { OnSelect = {} }
+ info.PrimaryEvent = 'OnSelect'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+
+ if (U:Typeof(info.Value or info.value) == 'Menu') then
+ info.Type = 'menu'
+ end
+
+ local item = CreateMenuItem(info)
+
+ if (info.Type == 'menu') then
+ item:On('select', function() item.Value() end)
+ end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about checkbox
+ ---@return Item New item
+ AddCheckbox = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'checkbox'
+ info.Value = U:Ensure(info.Value or info.value, false)
+ info.Events = { OnChange = {}, OnCheck = {}, OnUncheck = {} }
+ info.PrimaryEvent = 'OnCheck'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+ info.NewIndex = function(t, k, v)
+ if (k == 'Value') then
+ local value = U:Ensure(v, false)
+
+ if (value) then
+ t:Trigger('check', t)
+ else
+ t:Trigger('uncheck', t)
+ end
+ end
+ end
+
+ local item = CreateMenuItem(info)
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about slider
+ ---@return SliderItem New slider item
+ AddSlider = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'slider'
+ info.Events = { OnChange = {}, OnSelect = {} }
+ info.PrimaryEvent = 'OnSelect'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+
+ ---@class SliderItem : Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public AddValue fun(t: Item, info: table)
+ ---@field public AddValues fun(t: Item)
+ local item = CreateMenuItem(info)
+
+ --- Add a value to slider
+ ---@param info table Information about slider
+ function item:AddValue(info)
+ info = U:Ensure(info, {})
+
+ local value = {
+ Label = U:Ensure(info.Label or info.label, 'Value'),
+ Description = U:Ensure(info.Description or info.description, ''),
+ Value = info.Value or info.value
+ }
+
+ insert(self.Values, value)
+ end
+
+ --- Add values to slider
+ ---@vararg table[] List of values
+ function item:AddValues(...)
+ local arguments = pack(...)
+
+ for _, argument in pairs(arguments) do
+ if (U:Typeof(argument) == 'table') then
+ local hasIndex = argument[1] or nil
+
+ if (hasIndex and U:Typeof(hasIndex) == 'table') then
+ self:AddValues(unpack(argument))
+ else
+ self:AddValue(argument)
+ end
+ end
+ end
+ end
+
+ local values = U:Ensure(info.Values or info.values, {})
+
+ if (#values > 0) then
+ item:AddValues(values)
+ end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about range
+ ---@return RangeItem New Range item
+ AddRange = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'range'
+ info.Events = { OnChange = {}, OnSelect = {}, OnMin = {}, OnMax = {} }
+ info.PrimaryEvent = 'OnSelect'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+ info.Value = U:Ensure(info.Value or info.value, 0)
+ info.Min = U:Ensure(info.Min or info.min, 0)
+ info.Max = U:Ensure(info.Max or info.max, 0)
+ info.Validate = function(t, k, v)
+ if (k == 'Value' or k == 'value') then
+ v = U:Ensure(v, 0)
+
+ if (t.Min > v) then return false end
+ if (t.Max < v) then return false end
+ end
+
+ return true
+ end
+
+ if (info.Min > info.Max) then
+ local min = info.Min
+ local max = info.Max
+
+ info.Min = min
+ info.Max = max
+ end
+
+ if (info.Value < info.Min) then info.Value = info.Min end
+ if (info.Value > info.Max) then info.Value = info.Max end
+
+ ---@class RangeItem : Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public SetMinValue fun(t: any)
+ ---@field public SetMaxValue fun(t: any)
+ local item = CreateMenuItem(info)
+
+ --- Update min value of range
+ ---@param input number Minimum value of Range
+ function item:SetMinValue(input)
+ input = U:Ensure(input, 0)
+
+ self.Min = input
+
+ if (self.Value < self.Min) then
+ self.Value = self.Min
+ end
+
+ if (self.Min > self.Max) then
+ self.Max = self.Min
+ end
+ end
+
+ --- Update max value of range
+ ---@param input number Minimum value of Range
+ function item:SetMaxValue(input)
+ input = U:Ensure(input, 0)
+
+ self.Min = input
+
+ if (self.Value > self.Max) then
+ self.Value = self.Max
+ end
+
+ if (self.Min < self.Max) then
+ self.Min = self.Max
+ end
+ end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about confirm
+ ---@return ConfirmItem New Confirm item
+ AddConfirm = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'confirm'
+ info.Value = U:Ensure(info.Value or info.value, false)
+ info.Events = { OnConfirm = {}, OnDeny = {}, OnChange = {} }
+ info.PrimaryEvent = 'OnConfirm'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+ info.NewIndex = function(t, k, v)
+ if (k == 'Value') then
+ local value = U:Ensure(v, false)
+
+ if (value) then
+ t:Trigger('confirm', t)
+ else
+ t:Trigger('deny', t)
+ end
+ end
+ end
+
+ ---@class ConfirmItem : Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public Confirm fun(t: Item)
+ ---@field public Deny fun(t: Item)
+ local item = CreateMenuItem(info)
+
+ --- Confirm this item
+ function item:Confirm() item.Value = true end
+ --- Deny this item
+ function item:Deny() item.Value = false end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ --- Create child menu from properties of this object
+ ---@param t Menu|string MenuV menu
+ ---@param overrides table Properties to override in menu object (ignore parent)
+ ---@param namespace string Namespace of menu
+ InheritMenu = function(t, overrides, namespace)
+ return MenuV:InheritMenu(t, overrides, namespace)
+ end,
+ --- Add control key for specific menu
+ ---@param t Menu|string MenuV menu
+ ---@param action string Name of action
+ ---@param func function This will be executed
+ ---@param description string Key description
+ ---@param defaultType string Default key type
+ ---@param defaultKey string Default key
+ AddControlKey = function(t, action, func, description, defaultType, defaultKey)
+ if (U:Typeof(t.Namespace) ~= 'string' or t.Namespace == 'unknown') then
+ error('[MenuV] Namespace is required for assigning keys.')
+ return
+ end
+
+ MenuV:AddControlKey(t, action, func, description, defaultType, defaultKey)
+ end,
+ --- Assign key for opening this menu
+ ---@param t Menu|string MenuV menu
+ ---@param defaultType string Default key type
+ ---@param defaultKey string Default key
+ OpenWith = function(t, defaultType, defaultKey)
+ t:AddControlKey('open', function(m)
+ MenuV:CloseAll(function()
+ MenuV:OpenMenu(m)
+ end)
+ end, MenuV:T('open_menu'):format(t.Namespace), defaultType, defaultKey)
+ end,
+ --- Change title of menu
+ ---@param t Menu
+ ---@param title string Title of menu
+ SetTitle = function(t, title)
+ t.Title = U:Ensure(title, 'MenuV')
+ end,
+ --- Change subtitle of menu
+ ---@param t Menu
+ ---@param subtitle string Subtitle of menu
+ SetSubtitle = function(t, subtitle)
+ t.Subtitle = U:Ensure(subtitle, '')
+ end,
+ --- Change subtitle of menu
+ ---@param t Menu
+ ---@param position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
+ SetPosition = function(t, position)
+ t.Position = U:Ensure(position, 'topleft')
+ end,
+ --- Clear all Menu items
+ ---@param t Menu
+ ClearItems = function(t, update)
+ update = U:Ensure(update, true)
+
+ local items = CreateEmptyItemsTable({})
+
+ items(function(_, trigger, key, index, value, oldValue)
+ t:Trigger(trigger, key, index, value, oldValue)
+ end)
+
+ rawset(t.data, 'Items', items)
+
+ if (update and t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', 'Items', items)
+ end
+ end,
+ Open = function(t)
+ MenuV:OpenMenu(t)
+ end,
+ Close = function(t)
+ MenuV:CloseMenu(t)
+ end,
+ --- @see Menu to @see table
+ ---@param t Menu
+ ---@return table
+ ToTable = function(t)
+ local tempTable = {
+ theme = U:Ensure(t.Theme, 'default'),
+ uuid = U:Ensure(t.UUID, '00000000-0000-0000-0000-000000000000'),
+ title = U:Ensure(t.Title, 'MenuV'),
+ subtitle = U:Ensure(t.Subtitle, ''),
+ position = U:Ensure(t.Position, 'topleft'),
+ size = U:Ensure(t.Size, 'size-110'),
+ dictionary = U:Ensure(t.Dictionary, 'menuv'),
+ texture = U:Ensure(t.Texture, 'default'),
+ color = {
+ r = U:Ensure(t.Color.R, 0),
+ g = U:Ensure(t.Color.G, 0),
+ b = U:Ensure(t.Color.B, 255)
+ },
+ items = {}
+ }
+
+ local items = rawget(t.data, 'Items')
+
+ if (items ~= nil and items.ToTable ~= nil) then
+ tempTable.items = items:ToTable()
+ end
+
+ if (tempTable.color.r <= 0) then tempTable.color.r = 0 end
+ if (tempTable.color.r >= 255) then tempTable.color.r = 255 end
+ if (tempTable.color.g <= 0) then tempTable.color.g = 0 end
+ if (tempTable.color.g >= 255) then tempTable.color.g = 255 end
+ if (tempTable.color.b <= 0) then tempTable.color.b = 0 end
+ if (tempTable.color.b >= 255) then tempTable.color.b = 255 end
+
+ return tempTable
+ end
+ }
+
+ if (lower(item.Texture) == 'default' and lower(item.Dictionary) == 'menuv' and theme == 'native') then
+ item.Texture = 'default_native'
+ end
+
+ item.Events.OnOpen = {}
+ item.Events.OnClose = {}
+ item.Events.OnSelect = {}
+ item.Events.OnUpdate = {}
+ item.Events.OnSwitch = {}
+ item.Events.OnChange = {}
+ item.Events.OnIChange = {}
+
+ if (not U:Any(item.Size, { 'size-100', 'size-110', 'size-125', 'size-150', 'size-175', 'size-200' }, 'value')) then
+ item.Size = 'size-110'
+ end
+
+ local mt = {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ ---@param t Menu
+ __tostring = function(t)
+ return encode(t:ToTable())
+ end,
+ __call = function(t)
+ MenuV:OpenMenu(t)
+ end,
+ __newindex = function(t, k, v)
+ local whitelisted = { 'Title', 'Subtitle', 'Position', 'Color', 'R', 'G', 'B', 'Size', 'Dictionary', 'Texture', 'Theme' }
+ local key = U:Ensure(k, 'unknown')
+ local oldValue = rawget(t.data, k)
+
+ if (not U:Any(key, whitelisted, 'value') and oldValue ~= nil) then
+ return
+ end
+
+ local checkInput = t.Validate ~= nil and type(t.Validate) == 'function'
+ local inputParser = t.Parser ~= nil and type(t.Parser) == 'function'
+ local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function'
+
+ if (checkInput) then
+ local result = t:Validate(key, v)
+ result = U:Ensure(result, true)
+
+ if (not result) then
+ return
+ end
+ end
+
+ if (inputParser) then
+ local parsedValue = t:Parser(key, v)
+
+ v = parsedValue or v
+ end
+
+ rawset(t.data, k, v)
+
+ if (key == 'Theme' or key == 'theme') then
+ local theme_value = string.lower(U:Ensure(v, 'default'))
+
+ if (theme_value == 'native') then
+ rawset(t.data, 'color', { R = 255, G = 255, B = 255 })
+
+ local texture = U:Ensure(rawget(t.data, 'Texture'), 'default')
+
+ if (texture == 'default') then
+ rawset(t.data, 'Texture', 'default_native')
+ end
+ elseif (theme_value == 'default') then
+ local texture = U:Ensure(rawget(t.data, 'Texture'), 'default')
+
+ if (texture == 'default_native') then
+ rawset(t.data, 'Texture', 'default')
+ end
+ end
+ end
+
+ if (updateIndexTrigger) then
+ t:NewIndex(key, v)
+ end
+
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', key, v, oldValue)
+ end
+ end,
+ __len = function(t)
+ return #t.Items
+ end,
+ __pairs = function(t)
+ return pairs(rawget(t.data, 'Items') or {})
+ end,
+ __ipairs = function(t)
+ return ipairs(rawget(t.data, 'Items') or {})
+ end,
+ __metatable = 'MenuV',
+ }
+
+ ---@class Menu
+ ---@field public IsOpen boolean `true` if menu is open, otherwise `false`
+ ---@field public UUID string UUID of Menu
+ ---@field public Title string Title of Menu
+ ---@field public Subtitle string Subtitle of Menu
+ ---@field public Position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
+ ---@field public Texture string Name of texture example: "default"
+ ---@field public Dictionary string Name of dictionary example: "menuv"
+ ---@field public Color table Color of Menu
+ ---@field private Events table List of registered `on` events
+ ---@field public Items Item[] List of items
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Menu, event: string, func: function|Menu): string
+ ---@field public RemoveOnEvent fun(t: Menu, event: string, uuid: string)
+ ---@field public Validate fun(t: Menu, k: string, v:any)
+ ---@field public NewIndex fun(t: Menu, k: string, v: any)
+ ---@field public Parser fun(t: Menu, k: string, v: any)
+ ---@field public AddButton fun(t: Menu, info: table):Item
+ ---@field public AddCheckbox fun(t: Menu, info: table):Item
+ ---@field public AddSlider fun(t: Menu, info: table):SliderItem
+ ---@field public AddRange fun(t: Menu, info: table):RangeItem
+ ---@field public AddConfirm fun(t: Menu, info: table):ConfirmItem
+ ---@field public AddControlKey fun(t: Menu, action: string, func: function, description: string, defaultType: string, defaultKey: string)
+ ---@field public OpenWith fun(t: Menu, defaultType: string, defaultKey: string)
+ ---@field public SetTitle fun(t: Menu, title: string)
+ ---@field public SetSubtitle fun(t: Menu, subtitle: string)
+ ---@field public SetPosition fun(t: Menu, position: string)
+ ---@field public ClearItems fun(t: Menu)
+ ---@field public Open fun(t: Menu)
+ ---@field public Close fun(t: Menu)
+ ---@field public ToTable fun(t: Menu):table
+ local menu = setmetatable({ data = item, __class = 'Menu', __type = 'Menu' }, mt)
+
+ menu.Items(function(items, trigger, key, index, value, oldValue)
+ menu:Trigger(trigger, key, index, value, oldValue)
+ end)
+
+ for k, v in pairs(info or {}) do
+ local key = U:Ensure(k, 'unknown')
+
+ if (key == 'unknown') then return end
+
+ menu:On(key, v)
+ end
+
+ LoadTextureDictionary(menu.Dictionary)
+
+ return menu
+end
+
+_ENV.CreateMenu = CreateMenu
+_G.CreateMenu = CreateMenu
+
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+local decode = assert(json.decode)
+
+--- FiveM globals
+local LoadResourceFile = assert(LoadResourceFile)
+
+--- MenuV globals
+---@type Utilities
+local Utilities = assert(Utilities)
+
+--- Empty translations table
+local translations = {}
+
+--- Load all translations
+local lang = Utilities:Ensure((Config or {}).Language, 'en')
+local translations_path = ('languages/%s.json'):format(lang)
+local translations_raw = LoadResourceFile('menuv', translations_path)
+
+if (translations_raw) then
+ local transFile = decode(translations_raw)
+
+ if (transFile) then translations = Utilities:Ensure(transFile.translations, {}) end
+end
+
+_ENV.translations = translations
+_G.translations = translations
+
+--- MenuV table
+local menuv_table = {
+ ---@type string
+ __class = 'MenuV',
+ ---@type string
+ __type = 'MenuV',
+ ---@type Menu|nil
+ CurrentMenu = nil,
+ ---@type string|nil
+ CurrentUpdateUUID = nil,
+ ---@type string
+ CurrentResourceName = GET_CURRENT_RESOURCE_NAME(),
+ ---@type boolean
+ Loaded = false,
+ ---@type Menu[]
+ Menus = {},
+ ---@type Menu[]
+ ParentMenus = {},
+ ---@type table
+ NUICallbacks = {},
+ ---@type table
+ Translations = translations,
+ ---@class keys
+ Keys = setmetatable({ data = {}, __class = 'MenuVKeys', __type = 'keys' }, {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ __newindex = function(t, actionHax, v)
+ actionHax = Utilities:Ensure(actionHax, 'unknown')
+
+ if (actionHax == 'unknown') then return end
+
+ local rawKey = rawget(t.data, actionHax)
+ local keyExists = rawKey ~= nil
+ local prevState = Utilities:Ensure((rawKey or {}).status, false)
+ local newState = Utilities:Ensure(v, false)
+
+ if (keyExists) then
+ rawset(t.data[actionHax], 'status', newState)
+
+ if (prevState ~= newState and newState) then
+ rawKey.func(rawKey.menu)
+ end
+ end
+ end,
+ __call = function(t, actionHax, m, actionFunc, inputType)
+ actionHax = Utilities:Ensure(actionHax, 'unknown')
+ m = Utilities:Typeof(m) == 'Menu' and m or nil
+ actionFunc = Utilities:Ensure(actionFunc, function() end)
+ inputType = Utilities:Ensure(inputType, 'KEYBOARD')
+ inputType = upper(inputType)
+
+ if (actionHax == 'unknown') then return end
+
+ local rawKey = rawget(t.data, actionHax)
+ local keyExists = rawKey ~= nil
+
+ if (keyExists) then
+ if not rawKey.inputTypes[inputType] then
+ rawKey.inputTypes[inputType] = true
+ end
+
+ return
+ end
+
+ rawset(t.data, actionHax, { status = false, menu = m, func = actionFunc, inputTypes = { [inputType] = true } })
+ end
+ })
+}
+
+---@class MenuV
+MenuV = setmetatable(menuv_table, {})
+
+--- Send a NUI message to MenuV resource
+---@param input any
+local SEND_NUI_MESSAGE = function(input)
+ exports['menuv']:SendNUIMessage(input)
+end
+
+--- Register a NUI callback event
+---@param name string Name of callback
+---@param cb function Callback to execute
+local REGISTER_NUI_CALLBACK = function(name, cb)
+ name = Utilities:Ensure(name, 'unknown')
+ cb = Utilities:Ensure(cb, function(_, cb) cb('ok') end)
+
+ MenuV.NUICallbacks[name] = cb
+end
+
+--- Load translation
+---@param k string Translation key
+---@return string Translation or 'MISSING TRANSLATION'
+function MenuV:T(k)
+ k = Utilities:Ensure(k, 'unknown')
+
+ return Utilities:Ensure(MenuV.Translations[k], 'MISSING TRANSLATION')
+end
+
+--- Create a `MenuV` menu
+---@param title string Title of Menu
+---@param subtitle string Subtitle of Menu
+---@param position string Position of Menu
+---@param r number 0-255 RED
+---@param g number 0-255 GREEN
+---@param b number 0-255 BLUE
+---@param size string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'"
+---@param texture string Name of texture example: "default"
+---@param dictionary string Name of dictionary example: "menuv"
+---@param namespace string Namespace of Menu
+---@param theme string Theme of Menu
+---@return Menu
+function MenuV:CreateMenu(title, subtitle, position, r, g, b, size, texture, dictionary, namespace, theme)
+ local menu = CreateMenu({
+ Theme = theme,
+ Title = title,
+ Subtitle = subtitle,
+ Position = position,
+ R = r,
+ G = g,
+ B = b,
+ Size = size,
+ Texture = texture,
+ Dictionary = dictionary,
+ Namespace = namespace
+ })
+
+ local index = #(self.Menus or {}) + 1
+
+ insert(self.Menus, index, menu)
+
+ return self.Menus[index] or menu
+end
+
+--- Create a menu that inherits properties from another menu
+---@param parent Menu|string Menu or UUID of menu
+---@param overrides table Properties to override in menu object (ignore parent)
+---@param namespace string Namespace of menu
+function MenuV:InheritMenu(parent, overrides, namespace)
+ overrides = Utilities:Ensure(overrides, {})
+
+ local uuid = Utilities:Typeof(parent) == 'Menu' and parent.UUID or Utilities:Typeof(parent) == 'string' and parent
+
+ if (uuid == nil) then return end
+
+ local parentMenu = self:GetMenu(uuid)
+
+ if (parentMenu == nil) then return end
+
+ local menu = CreateMenu({
+ Theme = Utilities:Ensure(overrides.theme or overrides.Theme, parentMenu.Theme),
+ Title = Utilities:Ensure(overrides.title or overrides.Title, parentMenu.Title),
+ Subtitle = Utilities:Ensure(overrides.subtitle or overrides.Subtitle, parentMenu.Subtitle),
+ Position = Utilities:Ensure(overrides.position or overrides.Position, parentMenu.Position),
+ R = Utilities:Ensure(overrides.r or overrides.R, parentMenu.Color.R),
+ G = Utilities:Ensure(overrides.g or overrides.G, parentMenu.Color.G),
+ B = Utilities:Ensure(overrides.b or overrides.B, parentMenu.Color.B),
+ Size = Utilities:Ensure(overrides.size or overrides.Size, parentMenu.Size),
+ Texture = Utilities:Ensure(overrides.texture or overrides.Texture, parentMenu.Texture),
+ Dictionary = Utilities:Ensure(overrides.dictionary or overrides.Dictionary, parentMenu.Dictionary),
+ Namespace = Utilities:Ensure(namespace, 'unknown')
+ })
+
+ local index = #(self.Menus or {}) + 1
+
+ insert(self.Menus, index, menu)
+
+ return self.Menus[index] or menu
+end
+
+--- Load a menu based on `uuid`
+---@param uuid string UUID of menu
+---@return Menu|nil Founded menu or `nil`
+function MenuV:GetMenu(uuid)
+ uuid = Utilities:Ensure(uuid, '00000000-0000-0000-0000-000000000000')
+
+ for _, v in pairs(self.Menus) do
+ if (v.UUID == uuid) then
+ return v
+ end
+ end
+
+ return nil
+end
+
+--- Open a menu
+---@param menu Menu|string Menu or UUID of Menu
+---@param cb function Execute this callback when menu has opened
+function MenuV:OpenMenu(menu, cb, reopen)
+ local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
+
+ if (uuid == nil) then return end
+
+ cb = Utilities:Ensure(cb, function() end)
+
+ menu = self:GetMenu(uuid)
+
+ if (menu == nil) then return end
+
+ local dictionaryLoaded = HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary)
+
+ if (not self.Loaded or not dictionaryLoaded) then
+ if (not dictionaryLoaded) then REQUEST_STREAMED_TEXTURE_DICT(menu.Dictionary) end
+
+ CreateThread(function()
+ repeat Wait(0) until MenuV.Loaded
+
+ if (not dictionaryLoaded) then
+ repeat Wait(10) until HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary)
+ end
+
+ MenuV:OpenMenu(uuid, cb)
+ end)
+ return
+ end
+
+ if (self.CurrentMenu ~= nil) then
+ insert(self.ParentMenus, self.CurrentMenu)
+
+ self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
+ self.CurrentMenu:DestroyThreads()
+ end
+
+ self.CurrentMenu = menu
+ self.CurrentUpdateUUID = menu:On('update', function(m, k, v)
+ k = Utilities:Ensure(k, 'unknown')
+
+ if (k == 'Title' or k == 'title') then
+ SEND_NUI_MESSAGE({ action = 'UPDATE_TITLE', title = Utilities:Ensure(v, 'MenuV'), __uuid = m.UUID })
+ elseif (k == 'Subtitle' or k == 'subtitle') then
+ SEND_NUI_MESSAGE({ action = 'UPDATE_SUBTITLE', subtitle = Utilities:Ensure(v, ''), __uuid = m.UUID })
+ elseif (k == 'Items' or k == 'items') then
+ SEND_NUI_MESSAGE({ action = 'UPDATE_ITEMS', items = (m.Items:ToTable() or {}), __uuid = m.UUID })
+ elseif (k == 'Item' or k == 'item' and Utilities:Typeof(v) == 'Item') then
+ SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID })
+ elseif (k == 'AddItem' or k == 'additem' and Utilities:Typeof(v) == 'Item') then
+ SEND_NUI_MESSAGE({ action = 'ADD_ITEM', item = m.Items:ItemToTable(v), __uuid = m.UUID })
+ elseif (k == 'RemoveItem' or k == 'removeitem' and Utilities:Typeof(v) == 'Item') then
+ SEND_NUI_MESSAGE({ action = 'REMOVE_ITEM', uuid = v.UUID, __uuid = m.UUID })
+ elseif (k == 'UpdateItem' or k == 'updateitem' and Utilities:Typeof(v) == 'Item') then
+ SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID })
+ end
+ end)
+
+ SEND_NUI_MESSAGE({
+ action = 'OPEN_MENU',
+ menu = menu:ToTable(),
+ reopen = Utilities:Ensure(reopen, false)
+ })
+
+ cb()
+end
+
+function MenuV:Refresh()
+ if (self.CurrentMenu == nil) then
+ return
+ end
+
+ SEND_NUI_MESSAGE({
+ action = 'REFRESH_MENU',
+ menu = self.CurrentMenu:ToTable()
+ })
+end
+
+--- Close a menu
+---@param menu Menu|string Menu or UUID of Menu
+---@param cb function Execute this callback when menu has is closed or parent menu has opened
+function MenuV:CloseMenu(menu, cb)
+ local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
+
+ if (uuid == nil) then cb() return end
+
+ cb = Utilities:Ensure(cb, function() end)
+ menu = self:GetMenu(uuid)
+
+ if (menu == nil or self.CurrentMenu == nil or self.CurrentMenu.UUID ~= uuid) then cb() return end
+
+ self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
+ self.CurrentMenu:Trigger('close')
+ self.CurrentMenu:DestroyThreads()
+ self.CurrentMenu = nil
+
+ SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid })
+
+ if (#self.ParentMenus <= 0) then cb() return end
+
+ local prev_index = #self.ParentMenus
+ local prev_menu = self.ParentMenus[prev_index] or nil
+
+ if (prev_menu == nil) then cb() return end
+
+ remove(self.ParentMenus, prev_index)
+
+ self:OpenMenu(prev_menu, function()
+ cb()
+ end, true)
+end
+
+--- Close all menus
+---@param cb function Execute this callback when all menus are closed
+function MenuV:CloseAll(cb)
+ cb = Utilities:Ensure(cb, function() end)
+
+ if (not self.Loaded) then
+ CreateThread(function()
+ repeat Wait(0) until MenuV.Loaded
+
+ MenuV:CloseAll(cb)
+ end)
+ return
+ end
+
+ if (self.CurrentMenu == nil) then cb() return end
+
+ local uuid = Utilities:Ensure(self.CurrentMenu.UUID, '00000000-0000-0000-0000-000000000000')
+
+ self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
+ self.CurrentMenu:Trigger('close')
+ self.CurrentMenu:DestroyThreads()
+
+ SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid })
+
+ self.CurrentMenu = nil
+ self.ParentMenus = {}
+
+ cb()
+end
+
+--- Register keybind for specific menu
+---@param menu Menu|string MenuV menu
+---@param action string Name of action
+---@param func function This will be executed
+---@param description string Key description
+---@param defaultType string Default key type
+---@param defaultKey string Default key
+function MenuV:AddControlKey(menu, action, func, description, defaultType, defaultKey)
+ local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
+
+ action = Utilities:Ensure(action, 'UNKNOWN')
+ func = Utilities:Ensure(func, function() end)
+ description = Utilities:Ensure(description, 'unknown')
+ defaultType = Utilities:Ensure(defaultType, 'KEYBOARD')
+ defaultType = upper(defaultType)
+ defaultKey = Utilities:Ensure(defaultKey, 'F12')
+
+ local m = self:GetMenu(uuid)
+
+ if (m == nil) then return end
+
+ if (Utilities:Typeof(m.Namespace) ~= 'string' or m.Namespace == 'unknown') then
+ error('[MenuV] Namespace is required for assigning keys.')
+ return
+ end
+
+ action = Utilities:Replace(action, ' ', '_')
+ action = upper(action)
+
+ local resourceName = Utilities:Ensure(self.CurrentResourceName, 'unknown')
+ local namespace = Utilities:Ensure(m.Namespace, 'unknown')
+ local actionHash = GET_HASH_KEY(('%s_%s_%s'):format(resourceName, namespace, action))
+ local actionHax = format('%x', actionHash)
+
+ local typeGroup = Utilities:GetInputTypeGroup(defaultType)
+
+ if (self.Keys[actionHax] and self.Keys[actionHax].inputTypes[typeGroup]) then return end
+
+ self.Keys(actionHax, m, func, typeGroup)
+
+ local k = actionHax
+
+ if typeGroup > 0 then
+ local inputGroupName = Utilities:GetInputGroupName(typeGroup)
+ k = ('%s_%s'):format(lower(inputGroupName), k)
+ end
+
+ REGISTER_KEY_MAPPING(('+%s'):format(k), description, defaultType, defaultKey)
+ REGISTER_COMMAND(('+%s'):format(k), function() MenuV.Keys[actionHax] = true end)
+ REGISTER_COMMAND(('-%s'):format(k), function() MenuV.Keys[actionHax] = false end)
+end
+
+--- Checks if namespace is available
+---@param namespace string Namespace
+---@return boolean Returns `true` if given namespace is available
+function MenuV:IsNamespaceAvailable(namespace)
+ namespace = lower(Utilities:Ensure(namespace, 'unknown'))
+
+ if (namespace == 'unknown') then return true end
+
+ ---@param v Menu
+ for k, v in pairs(self.Menus or {}) do
+ local v_namespace = Utilities:Ensure(v.Namespace, 'unknown')
+
+ if (namespace == lower(v_namespace)) then
+ return false
+ end
+ end
+
+ return true
+end
+
+
+--- Mark MenuV as loaded when `main` resource is loaded
+exports['menuv']:IsLoaded(function()
+ MenuV.Loaded = true
+end)
+
+--- Register callback handler for MenuV
+exports('NUICallback', function(name, info, cb)
+ name = Utilities:Ensure(name, 'unknown')
+
+ if (MenuV.NUICallbacks == nil or MenuV.NUICallbacks[name] == nil) then
+ return
+ end
+
+ MenuV.NUICallbacks[name](info, cb)
+end)
+
+REGISTER_NUI_CALLBACK('open', function(info, cb)
+ local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
+ local new_uuid = Utilities:Ensure(info.new_uuid, '00000000-0000-0000-0000-000000000000')
+
+ cb('ok')
+
+ if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID == uuid or MenuV.CurrentMenu.UUID == new_uuid) then return end
+
+ for _, v in pairs(MenuV.ParentMenus) do
+ if (v.UUID == uuid) then
+ return
+ end
+ end
+
+ MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
+ MenuV.CurrentMenu:Trigger('close')
+ MenuV.CurrentMenu:DestroyThreads()
+ MenuV.CurrentMenu = nil
+ MenuV.ParentMenus = {}
+end)
+
+REGISTER_NUI_CALLBACK('opened', function(info, cb)
+ local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
+
+ cb('ok')
+
+ if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then return end
+
+ MenuV.CurrentMenu:Trigger('open')
+end)
+
+REGISTER_NUI_CALLBACK('submit', function(info, cb)
+ local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
+
+ cb('ok')
+
+ if (MenuV.CurrentMenu == nil) then return end
+
+ for k, v in pairs(MenuV.CurrentMenu.Items) do
+ if (v.UUID == uuid) then
+ if (v.__type == 'confirm' or v.__type == 'checkbox') then
+ v.Value = Utilities:Ensure(info.value, false)
+ elseif (v.__type == 'range') then
+ v.Value = Utilities:Ensure(info.value, v.Min)
+ elseif (v.__type == 'slider') then
+ v.Value = Utilities:Ensure(info.value, 0) + 1
+ end
+
+ MenuV.CurrentMenu:Trigger('select', v)
+
+ if (v.__type == 'button' or v.__type == 'menu') then
+ MenuV.CurrentMenu.Items[k]:Trigger('select')
+ elseif (v.__type == 'range') then
+ MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value)
+ elseif (v.__type == 'slider') then
+ local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil
+
+ if (option == nil) then return end
+
+ MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value)
+ end
+
+ return
+ end
+ end
+end)
+
+REGISTER_NUI_CALLBACK('close', function(info, cb)
+ local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
+
+ if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then cb('ok') return end
+
+ MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
+ MenuV.CurrentMenu:Trigger('close')
+ MenuV.CurrentMenu:DestroyThreads()
+ MenuV.CurrentMenu = nil
+
+ if (#MenuV.ParentMenus <= 0) then cb('ok') return end
+
+ local prev_index = #MenuV.ParentMenus
+ local prev_menu = MenuV.ParentMenus[prev_index] or nil
+
+ if (prev_menu == nil) then cb('ok') return end
+
+ remove(MenuV.ParentMenus, prev_index)
+
+ MenuV:OpenMenu(prev_menu, function()
+ cb('ok')
+ end, true)
+end)
+
+REGISTER_NUI_CALLBACK('close_all', function(info, cb)
+ if (MenuV.CurrentMenu == nil) then cb('ok') return end
+
+ MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
+ MenuV.CurrentMenu:Trigger('close')
+ MenuV.CurrentMenu:DestroyThreads()
+ MenuV.CurrentMenu = nil
+ MenuV.ParentMenus = {}
+
+ cb('ok')
+end)
+
+REGISTER_NUI_CALLBACK('switch', function(info, cb)
+ local prev_uuid = Utilities:Ensure(info.prev, '00000000-0000-0000-0000-000000000000')
+ local next_uuid = Utilities:Ensure(info.next, '00000000-0000-0000-0000-000000000000')
+ local prev_item, next_item = nil, nil
+
+ cb('ok')
+
+ if (MenuV.CurrentMenu == nil) then return end
+
+ for k, v in pairs(MenuV.CurrentMenu.Items) do
+ if (v.UUID == prev_uuid) then
+ prev_item = v
+
+ MenuV.CurrentMenu.Items[k]:Trigger('leave')
+ end
+
+ if (v.UUID == next_uuid) then
+ next_item = v
+
+ MenuV.CurrentMenu.Items[k]:Trigger('enter')
+ end
+ end
+
+ if (prev_item ~= nil and next_item ~= nil) then
+ MenuV.CurrentMenu:Trigger('switch', next_item, prev_item)
+ end
+end)
+
+REGISTER_NUI_CALLBACK('update', function(info, cb)
+ local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
+
+ cb('ok')
+
+ if (MenuV.CurrentMenu == nil) then return end
+
+ for k, v in pairs(MenuV.CurrentMenu.Items) do
+ if (v.UUID == uuid) then
+ local newValue, oldValue = nil, nil
+
+ if (v.__type == 'confirm' or v.__type == 'checkbox') then
+ newValue = Utilities:Ensure(info.now, false)
+ oldValue = Utilities:Ensure(info.prev, false)
+ elseif (v.__type == 'range') then
+ newValue = Utilities:Ensure(info.now, v.Min)
+ oldValue = Utilities:Ensure(info.prev, v.Min)
+ elseif (v.__type == 'slider') then
+ newValue = Utilities:Ensure(info.now, 0) + 1
+ oldValue = Utilities:Ensure(info.prev, 0) + 1
+ end
+
+ if (Utilities:Any(v.__type, { 'button', 'menu', 'label' }, 'value')) then return end
+
+ MenuV.CurrentMenu:Trigger('update', v, newValue, oldValue)
+ MenuV.CurrentMenu.Items[k]:Trigger('change', newValue, oldValue)
+
+ if (v.SaveOnUpdate) then
+ MenuV.CurrentMenu.Items[k].Value = newValue
+
+ if (v.__type == 'range') then
+ MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value)
+ elseif (v.__type == 'slider') then
+ local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil
+
+ if (option == nil) then return end
+
+ MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value)
+ end
+ end
+ return
+ end
+ end
+end)
diff --git a/resources/[standalone]/menuv/menuv/components/item.lua b/resources/[standalone]/menuv/menuv/components/item.lua
new file mode 100644
index 0000000..5cedbe8
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv/components/item.lua
@@ -0,0 +1,273 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+---@type Utilities
+local U = assert(Utilities)
+local type = assert(type)
+local pairs = assert(pairs)
+local lower = assert(string.lower)
+local upper = assert(string.upper)
+local sub = assert(string.sub)
+local pack = assert(table.pack)
+local unpack = assert(table.unpack)
+local insert = assert(table.insert)
+local rawset = assert(rawset)
+local rawget = assert(rawget)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local CreateThread = assert(Citizen.CreateThread)
+
+--- Create a new menu item
+---@param info table Menu information
+---@return Item New item
+function CreateMenuItem(info)
+ info = U:Ensure(info, {})
+
+ local item = {
+ ---@type Menu|nil
+ __menu = U:Ensure(info.__Menu or info.__menu, { __class = 'Menu', __type = 'Menu' }, true) or nil,
+ ---@type string
+ __event = U:Ensure(info.PrimaryEvent or info.primaryEvent, 'unknown'),
+ ---@type string
+ UUID = U:UUID(),
+ ---@type string
+ Icon = U:Ensure(info.Icon or info.icon, 'none'),
+ ---@type string
+ Label = U:Ensure(info.Label or info.label, ''),
+ ---@type string
+ Description = U:Ensure(info.Description or info.description, ''),
+ ---@type any
+ Value = info.Value or info.value,
+ ---@type table[]
+ Values = {},
+ ---@type number
+ Min = U:Ensure(info.Min or info.min, 0),
+ ---@type number
+ Max = U:Ensure(info.Max or info.max, 0),
+ ---@type boolean
+ Disabled = U:Ensure(info.Disabled or info.disabled, false),
+ ---@type table
+ Events = U:Ensure(info.Events or info.events, {}),
+ ---@type boolean
+ SaveOnUpdate = U:Ensure(info.SaveOnUpdate or info.saveOnUpdate, false),
+ ---@param t Item
+ ---@param event string Name of Event
+ Trigger = function(t, event, ...)
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ local args = pack(...)
+
+ for _, v in pairs(t.Events[event]) do
+ CreateThread(function()
+ v(t, unpack(args))
+ end)
+ end
+ end,
+ ---@param t Item
+ ---@param event string Name of event
+ ---@param func function|Menu Function or Menu to trigger
+ On = function(t, event, func)
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ local _type = U:Typeof(func)
+
+ if (_type == 'Menu') then
+ local menu_t = {
+ __class = 'function',
+ __type = 'function',
+ func = function(t) MenuV:OpenMenu(t.uuid) end,
+ uuid = func.UUID or func.uuid or U:UUID()
+ }
+ local menu_mt = { __index = menu_t, __call = function(t) t:func() end }
+ local menu_item = setmetatable(menu_t, menu_mt)
+
+ insert(t.Events[event], menu_item)
+
+ return
+ end
+
+ func = U:Ensure(func, function() end)
+
+ insert(t.Events[event], func)
+ end,
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Validate = U:Ensure(info.Validate or info.validate, function(t, k, v)
+ return true
+ end),
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Parser = U:Ensure(info.Parser or info.parser, function(t, k, v)
+ return v
+ end),
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v)
+ end),
+ ---@param t Item
+ ---@return any
+ GetValue = function(t)
+ local itemType = U:Ensure(t.__type, 'unknown')
+
+ if (itemType == 'button' or itemType == 'menu' or itemType == 'unknown') then
+ return t.Value
+ end
+
+ if (itemType == 'checkbox' or itemType == 'confirm') then
+ return U:Ensure(t.Value, false)
+ end
+
+ if (itemType == 'slider') then
+ for _, item in pairs(t.Values) do
+ if (item.Value == t.Value) then
+ return item.Value
+ end
+ end
+
+ return nil
+ end
+
+ if (itemType == 'range') then
+ local rawValue = U:Ensure(t.Value, 0)
+
+ if (t.Min > rawValue) then
+ return t.Min
+ end
+
+ if (t.Max < rawValue) then
+ return t.Max
+ end
+
+ return rawValue
+ end
+ end,
+ ---@return Menu|nil
+ GetParentMenu = function(t)
+ return t.__menu or nil
+ end
+ }
+
+ item.Events.OnEnter = {}
+ item.Events.OnLeave = {}
+ item.Events.OnUpdate = {}
+ item.Events.OnDestroy = {}
+
+ local mt = {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ __tostring = function(t)
+ return t.UUID
+ end,
+ __call = function(t, ...)
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger(t.__event, ...)
+ end
+ end,
+ __newindex = function(t, k, v)
+ local key = U:Ensure(k, 'unknown')
+ local oldValue = rawget(t.data, k)
+ local checkInput = t.Validate ~= nil and type(t.Validate) == 'function'
+ local inputParser = t.Parser ~= nil and type(t.Parser) == 'function'
+ local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function'
+
+ if (checkInput) then
+ local result = t:Validate(key, v)
+ result = U:Ensure(result, true)
+
+ if (not result) then
+ return
+ end
+ end
+
+ if (inputParser) then
+ local parsedValue = t:Parser(key, v)
+
+ v = parsedValue or v
+ end
+
+ rawset(t.data, k, v)
+
+ if (updateIndexTrigger) then
+ t:NewIndex(key, v)
+ end
+
+ if (t.__menu ~= nil and U:Typeof(t.__menu) == 'Menu' and t.__menu.Trigger ~= nil and U:Typeof( t.__menu.Trigger) == 'function') then
+ t.__menu:Trigger('update', 'UpdateItem', t)
+ end
+
+ if (key == 'Value' and t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', key, v, oldValue)
+ end
+ end,
+ __metatable = 'MenuV'
+ }
+
+ ---@class Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field public SaveOnUpdate boolean Save on `update`
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function|Menu)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public Parser fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public GetParentMenu func(t: Item):Menu|nil
+ local i = setmetatable({ data = item, __class = 'Item', __type = U:Ensure(info.Type or info.type, 'unknown') }, mt)
+
+ for k, v in pairs(info or {}) do
+ local key = U:Ensure(k, 'unknown')
+
+ if (key == 'unknown') then return end
+
+ i:On(key, v)
+ end
+
+ return i
+end
+
+_ENV.CreateMenuItem = CreateMenuItem
+_G.CreateMenuItem = CreateMenuItem
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/menuv/components/menu.lua b/resources/[standalone]/menuv/menuv/components/menu.lua
new file mode 100644
index 0000000..a8b3b31
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv/components/menu.lua
@@ -0,0 +1,1080 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+---@type Utilities
+local U = assert(Utilities)
+local type = assert(type)
+local next = assert(next)
+local pairs = assert(pairs)
+local ipairs = assert(ipairs)
+local lower = assert(string.lower)
+local upper = assert(string.upper)
+local sub = assert(string.sub)
+local insert = assert(table.insert)
+local remove = assert(table.remove)
+local pack = assert(table.pack)
+local unpack = assert(table.unpack)
+local encode = assert(json.encode)
+local rawset = assert(rawset)
+local rawget = assert(rawget)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
+local GET_INVOKING_RESOURCE = assert(GetInvokingResource)
+local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded)
+local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict)
+
+--- MenuV local variable
+local current_resource = GET_CURRENT_RESOURCE_NAME()
+
+--- Returns default empty table for items
+---@returns items
+function CreateEmptyItemsTable(data)
+ data = U:Ensure(data, {})
+ data.ToTable = function(t)
+ local tempTable = {}
+ local index = 0
+
+ for _, option in pairs(t) do
+ index = index + 1
+
+ tempTable[index] = {
+ index = index,
+ type = option.__type,
+ uuid = U:Ensure(option.UUID, 'unknown'),
+ icon = U:Ensure(option.Icon, 'none'),
+ label = U:Ensure(option.Label, 'Unknown'),
+ description = U:Ensure(option.Description, ''),
+ value = 'none',
+ values = {},
+ min = U:Ensure(option.Min, 0),
+ max = U:Ensure(option.Max, 0),
+ disabled = U:Ensure(option.Disabled, false)
+ }
+
+ if (option.__type == 'button' or option.__type == 'menu') then
+ tempTable[index].value = 'none'
+ elseif (option.__type == 'checkbox' or option.__type == 'confirm') then
+ tempTable[index].value = U:Ensure(option.Value, false)
+ elseif (option.__type == 'range') then
+ tempTable[index].value = U:Ensure(option.Value, 0)
+
+ if (tempTable[index].value <= tempTable[index].min) then
+ tempTable[index].value = tempTable[index].min
+ elseif (tempTable[index].value >= tempTable[index].max) then
+ tempTable[index].value = tempTable[index].max
+ end
+ elseif (option.__type == 'slider') then
+ tempTable[index].value = 0
+ end
+
+ local _values = U:Ensure(option.Values, {})
+ local vIndex = 0
+
+ for valueIndex, value in pairs(_values) do
+ vIndex = vIndex + 1
+
+ tempTable[index].values[vIndex] = {
+ label = U:Ensure(value.Label, 'Option'),
+ description = U:Ensure(value.Description, ''),
+ value = vIndex
+ }
+
+ if (option.__type == 'slider') then
+ if (U:Ensure(option.Value, 0) == valueIndex) then
+ tempTable[index].value = (valueIndex - 1)
+ end
+ end
+ end
+ end
+
+ return tempTable
+ end
+ data.ItemToTable = function(t, i)
+ local tempTable = {}
+ local index = 0
+ local uuid = U:Typeof(i) == 'Item' and i.UUID or U:Ensure(i, '00000000-0000-0000-0000-000000000000')
+
+ for _, option in pairs(t) do
+ index = index + 1
+
+ if (option.UUID == uuid) then
+ tempTable = {
+ index = index,
+ type = option.__type,
+ uuid = U:Ensure(option.UUID, 'unknown'),
+ icon = U:Ensure(option.Icon, 'none'),
+ label = U:Ensure(option.Label, 'Unknown'),
+ description = U:Ensure(option.Description, ''),
+ value = 'none',
+ values = {},
+ min = U:Ensure(option.Min, 0),
+ max = U:Ensure(option.Max, 0),
+ disabled = U:Ensure(option.Disabled, false)
+ }
+
+ if (option.__type == 'button' or option.__type == 'menu') then
+ tempTable.value = 'none'
+ elseif (option.__type == 'checkbox' or option.__type == 'confirm') then
+ tempTable.value = U:Ensure(option.Value, false)
+ elseif (option.__type == 'range') then
+ tempTable.value = U:Ensure(option.Value, 0)
+
+ if (tempTable.value <= tempTable.min) then
+ tempTable.value = tempTable.min
+ elseif (tempTable.value >= tempTable.max) then
+ tempTable.value = tempTable.max
+ end
+ elseif (option.__type == 'slider') then
+ tempTable.value = 0
+ end
+
+ local _values = U:Ensure(option.Values, {})
+ local vIndex = 0
+
+ for valueIndex, value in pairs(_values) do
+ vIndex = vIndex + 1
+
+ tempTable.values[vIndex] = {
+ label = U:Ensure(value.Label, 'Option'),
+ description = U:Ensure(value.Description, ''),
+ value = vIndex
+ }
+
+ if (option.__type == 'slider') then
+ if (U:Ensure(option.Value, 0) == valueIndex) then
+ tempTable.value = (valueIndex - 1)
+ end
+ end
+ end
+
+ return tempTable
+ end
+ end
+
+ return tempTable
+ end
+ data.AddItem = function(t, item)
+ if (U:Typeof(item) == 'Item') then
+ local newIndex = #(U:Ensure(rawget(t, 'data'), {})) + 1
+
+ rawset(t.data, newIndex, item)
+
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', 'AddItem', item)
+ end
+ end
+
+ return U:Ensure(rawget(t, 'data'), {})
+ end
+
+ local item_pairs = function(t, k)
+ local _k, _v = next((rawget(t, 'data') or {}), k)
+
+ if (_v ~= nil and type(_v) ~= 'table') then
+ return item_pairs(t, _k)
+ end
+
+ return _k, _v
+ end
+
+ local item_ipairs = function(t, k)
+ local _k, _v = next((rawget(t, 'data') or {}), k)
+
+ if (_v ~= nil and (type(_v) ~= 'table' or type(_k) ~= 'number')) then
+ return item_ipairs(t, _k)
+ end
+
+ return _k, _v
+ end
+
+ _G.item_pairs = item_pairs
+ _ENV.item_pairs = item_pairs
+
+ ---@class items
+ return setmetatable({ data = data, Trigger = nil }, {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ __newindex = function(t, k, v)
+ local oldValue = rawget(t.data, k)
+
+ rawset(t.data, k, v)
+
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ if (oldValue == nil) then
+ t:Trigger('update', 'AddItem', v)
+ elseif (oldValue ~= nil and v == nil) then
+ t:Trigger('update', 'RemoveItem', oldValue)
+ elseif (oldValue ~= v) then
+ t:Trigger('update', 'UpdateItem', v, oldValue)
+ end
+ end
+ end,
+ __call = function(t, func)
+ rawset(t, 'Trigger', U:Ensure(func, function() end))
+ end,
+ __pairs = function(t)
+ local k = nil
+
+ return function()
+ local v
+
+ k, v = item_pairs(t, k)
+
+ return k, v
+ end, t, nil
+ end,
+ __ipairs = function(t)
+ local k = nil
+
+ return function()
+ local v
+
+ k, v = item_ipairs(t, k)
+
+ return k, v
+ end, t, 0
+ end,
+ __len = function(t)
+ local items = U:Ensure(rawget(t, 'data'), {})
+ local itemCount = 0
+
+ for _, v in pairs(items) do
+ if (U:Typeof(v) == 'Item') then
+ itemCount = itemCount + 1
+ end
+ end
+
+ return itemCount
+ end
+ })
+end
+
+--- Load a texture dictionary if not already loaded
+---@param textureDictionary string Name of texture dictionary
+local function LoadTextureDictionary(textureDictionary)
+ textureDictionary = U:Ensure(textureDictionary, 'menuv')
+
+ if (HAS_STREAMED_TEXTURE_DICT_LOADED(textureDictionary)) then return end
+
+ REQUEST_STREAMED_TEXTURE_DICT(textureDictionary, true)
+end
+
+--- Create a new menu item
+---@param info table Menu information
+---@return Menu New item
+function CreateMenu(info)
+ info = U:Ensure(info, {})
+
+ local namespace = U:Ensure(info.Namespace or info.namespace, 'unknown')
+ local namespace_available = MenuV:IsNamespaceAvailable(namespace)
+
+ if (not namespace_available) then
+ error(("[MenuV] Namespace '%s' is already taken, make sure it is unique."):format(namespace))
+ end
+
+ local theme = lower(U:Ensure(info.Theme or info.theme, 'default'))
+
+ if (theme ~= 'default' and theme ~= 'native') then
+ theme = 'default'
+ end
+
+ if (theme == 'native') then
+ info.R, info.G, info.B = 255, 255, 255
+ info.r, info.g, info.b = 255, 255, 255
+ end
+
+ local item = {
+ ---@type string
+ Namespace = namespace,
+ ---@type boolean
+ IsOpen = false,
+ ---@type string
+ UUID = U:UUID(),
+ ---@type string
+ Title = not (info.Title or info.title) and ' ' or U:Ensure(info.Title or info.title, 'MenuV'),
+ ---@type string
+ Subtitle = U:Ensure(info.Subtitle or info.subtitle, ''),
+ ---@type string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
+ Position = U:Ensure(info.Position or info.position, 'topleft'),
+ ---@type table
+ Color = {
+ R = U:Ensure(info.R or info.r, 0),
+ G = U:Ensure(info.G or info.g, 0),
+ B = U:Ensure(info.B or info.b, 255)
+ },
+ ---@type string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'"
+ Size = U:Ensure(info.Size or info.size, 'size-110'),
+ ---@type string
+ Dictionary = U:Ensure(info.Dictionary or info.dictionary, 'menuv'),
+ ---@type string
+ Texture = U:Ensure(info.Texture or info.texture, 'default'),
+ ---@type table
+ Events = U:Ensure(info.Events or info.events, {}),
+ ---@type string
+ Theme = theme,
+ ---@type Item[]
+ Items = CreateEmptyItemsTable({}),
+ ---@param t Menu
+ ---@param event string Name of Event
+ Trigger = function(t, event, ...)
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ if (event == 'OnOpen') then rawset(t, 'IsOpen', true)
+ elseif (event == 'OnClose') then rawset(t, 'IsOpen', false) end
+
+ local args = pack(...)
+
+ for _, v in pairs(t.Events[event]) do
+ if (type(v) == 'table' and U:Typeof(v.func) == 'function') then
+ CreateThread(function()
+ if (event == 'OnClose') then
+ v.func(t, unpack(args))
+ else
+ local threadId = coroutine.running()
+
+ if (threadId ~= nil) then
+ insert(t.data.Threads, threadId)
+
+ v.func(t, unpack(args))
+
+ for i = 0, #(t.data.Threads or {}), 1 do
+ if (t.data.Threads[i] == threadId) then
+ remove(t.data.Threads, i)
+ return
+ end
+ end
+ end
+ end
+ end)
+ end
+ end
+ end,
+ ---@type thread[]
+ Threads = {},
+ ---@param t Menu
+ DestroyThreads = function(t)
+ for _, threadId in pairs(t.data.Threads or {}) do
+ local threadStatus = coroutine.status(threadId)
+
+ if (threadStatus ~= nil and threadStatus ~= 'dead') then
+ coroutine.close(threadId)
+ end
+ end
+
+ t.data.Threads = {}
+ end,
+ ---@param t Menu
+ ---@param event string Name of event
+ ---@param func function|Menu Function or Menu to trigger
+ ---@return string UUID of event
+ On = function(t, event, func)
+ local ir = GET_INVOKING_RESOURCE()
+ local resource = U:Ensure(ir, current_resource)
+
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ func = U:Ensure(func, function() end)
+
+ local uuid = U:UUID()
+
+ insert(t.Events[event], {
+ __uuid = uuid,
+ __resource = resource,
+ func = func
+ })
+
+ return uuid
+ end,
+ ---@param t Menu
+ ---@param event string Name of event
+ ---@param uuid string UUID of event
+ RemoveOnEvent = function(t, event, uuid)
+ local ir = GET_INVOKING_RESOURCE()
+ local resource = U:Ensure(ir, current_resource)
+
+ event = lower(U:Ensure(event, 'unknown'))
+
+ if (event == 'unknown') then return end
+ if (U:StartsWith(event, 'on')) then
+ event = 'On' .. sub(event, 3):gsub('^%l', upper)
+ else
+ event = 'On' .. event:gsub('^%l', upper)
+ end
+
+ if (not U:Any(event, (t.Events or {}), 'key')) then
+ return
+ end
+
+ uuid = U:Ensure(uuid, '00000000-0000-0000-0000-000000000000')
+
+ for i = 1, #t.Events[event], 1 do
+ if (t.Events[event][i] ~= nil and
+ t.Events[event][i].__uuid == uuid and
+ t.Events[event][i].__resource == resource) then
+ remove(t.Events[event], i)
+ end
+ end
+ end,
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Validate = U:Ensure(info.Validate or info.validate, function(t, k, v)
+ return true
+ end),
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ Parser = function(t, k, v)
+ if (k == 'Position' or k == 'position') then
+ local position = lower(U:Ensure(v, 'topleft'))
+
+ if (U:Any(position, {'topleft', 'topcenter', 'topright', 'centerleft', 'center', 'centerright', 'bottomleft', 'bottomcenter', 'bottomright'}, 'value')) then
+ return position
+ else
+ return 'topleft'
+ end
+ end
+
+ return v
+ end,
+ ---@param t Item
+ ---@param k string
+ ---@param v string
+ NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v)
+ end),
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about button
+ ---@return Item New item
+ AddButton = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'button'
+ info.Events = { OnSelect = {} }
+ info.PrimaryEvent = 'OnSelect'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+
+ if (U:Typeof(info.Value or info.value) == 'Menu') then
+ info.Type = 'menu'
+ end
+
+ local item = CreateMenuItem(info)
+
+ if (info.Type == 'menu') then
+ item:On('select', function() item.Value() end)
+ end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about checkbox
+ ---@return Item New item
+ AddCheckbox = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'checkbox'
+ info.Value = U:Ensure(info.Value or info.value, false)
+ info.Events = { OnChange = {}, OnCheck = {}, OnUncheck = {} }
+ info.PrimaryEvent = 'OnCheck'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+ info.NewIndex = function(t, k, v)
+ if (k == 'Value') then
+ local value = U:Ensure(v, false)
+
+ if (value) then
+ t:Trigger('check', t)
+ else
+ t:Trigger('uncheck', t)
+ end
+ end
+ end
+
+ local item = CreateMenuItem(info)
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about slider
+ ---@return SliderItem New slider item
+ AddSlider = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'slider'
+ info.Events = { OnChange = {}, OnSelect = {} }
+ info.PrimaryEvent = 'OnSelect'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+
+ ---@class SliderItem : Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public AddValue fun(t: Item, info: table)
+ ---@field public AddValues fun(t: Item)
+ local item = CreateMenuItem(info)
+
+ --- Add a value to slider
+ ---@param info table Information about slider
+ function item:AddValue(info)
+ info = U:Ensure(info, {})
+
+ local value = {
+ Label = U:Ensure(info.Label or info.label, 'Value'),
+ Description = U:Ensure(info.Description or info.description, ''),
+ Value = info.Value or info.value
+ }
+
+ insert(self.Values, value)
+ end
+
+ --- Add values to slider
+ ---@vararg table[] List of values
+ function item:AddValues(...)
+ local arguments = pack(...)
+
+ for _, argument in pairs(arguments) do
+ if (U:Typeof(argument) == 'table') then
+ local hasIndex = argument[1] or nil
+
+ if (hasIndex and U:Typeof(hasIndex) == 'table') then
+ self:AddValues(unpack(argument))
+ else
+ self:AddValue(argument)
+ end
+ end
+ end
+ end
+
+ local values = U:Ensure(info.Values or info.values, {})
+
+ if (#values > 0) then
+ item:AddValues(values)
+ end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about range
+ ---@return RangeItem New Range item
+ AddRange = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'range'
+ info.Events = { OnChange = {}, OnSelect = {}, OnMin = {}, OnMax = {} }
+ info.PrimaryEvent = 'OnSelect'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+ info.Value = U:Ensure(info.Value or info.value, 0)
+ info.Min = U:Ensure(info.Min or info.min, 0)
+ info.Max = U:Ensure(info.Max or info.max, 0)
+ info.Validate = function(t, k, v)
+ if (k == 'Value' or k == 'value') then
+ v = U:Ensure(v, 0)
+
+ if (t.Min > v) then return false end
+ if (t.Max < v) then return false end
+ end
+
+ return true
+ end
+
+ if (info.Min > info.Max) then
+ local min = info.Min
+ local max = info.Max
+
+ info.Min = min
+ info.Max = max
+ end
+
+ if (info.Value < info.Min) then info.Value = info.Min end
+ if (info.Value > info.Max) then info.Value = info.Max end
+
+ ---@class RangeItem : Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public SetMinValue fun(t: any)
+ ---@field public SetMaxValue fun(t: any)
+ local item = CreateMenuItem(info)
+
+ --- Update min value of range
+ ---@param input number Minimum value of Range
+ function item:SetMinValue(input)
+ input = U:Ensure(input, 0)
+
+ self.Min = input
+
+ if (self.Value < self.Min) then
+ self.Value = self.Min
+ end
+
+ if (self.Min > self.Max) then
+ self.Max = self.Min
+ end
+ end
+
+ --- Update max value of range
+ ---@param input number Minimum value of Range
+ function item:SetMaxValue(input)
+ input = U:Ensure(input, 0)
+
+ self.Min = input
+
+ if (self.Value > self.Max) then
+ self.Value = self.Max
+ end
+
+ if (self.Min < self.Max) then
+ self.Min = self.Max
+ end
+ end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ ---@type function
+ ---@param t Menu MenuV menu
+ ---@param info table Information about confirm
+ ---@return ConfirmItem New Confirm item
+ AddConfirm = function(t, info)
+ info = U:Ensure(info, {})
+
+ info.Type = 'confirm'
+ info.Value = U:Ensure(info.Value or info.value, false)
+ info.Events = { OnConfirm = {}, OnDeny = {}, OnChange = {} }
+ info.PrimaryEvent = 'OnConfirm'
+ info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
+ info.__menu = t
+ info.NewIndex = function(t, k, v)
+ if (k == 'Value') then
+ local value = U:Ensure(v, false)
+
+ if (value) then
+ t:Trigger('confirm', t)
+ else
+ t:Trigger('deny', t)
+ end
+ end
+ end
+
+ ---@class ConfirmItem : Item
+ ---@filed private __event string Name of primary event
+ ---@field public UUID string UUID of Item
+ ---@field public Icon string Icon/Emoji for Item
+ ---@field public Label string Label of Item
+ ---@field public Description string Description of Item
+ ---@field public Value any Value of Item
+ ---@field public Values table[] List of values
+ ---@field public Min number Min range value
+ ---@field public Max number Max range value
+ ---@field public Disabled boolean Disabled state of Item
+ ---@field private Events table List of registered `on` events
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Item, event: string, func: function)
+ ---@field public Validate fun(t: Item, k: string, v:any)
+ ---@field public NewIndex fun(t: Item, k: string, v: any)
+ ---@field public GetValue fun(t: Item):any
+ ---@field public Confirm fun(t: Item)
+ ---@field public Deny fun(t: Item)
+ local item = CreateMenuItem(info)
+
+ --- Confirm this item
+ function item:Confirm() item.Value = true end
+ --- Deny this item
+ function item:Deny() item.Value = false end
+
+ if (info.TriggerUpdate) then
+ t.Items:AddItem(item)
+ else
+ local items = rawget(t.data, 'Items')
+
+ if (items) then
+ local newIndex = #items + 1
+
+ rawset(items.data, newIndex, item)
+
+ return items.data[newIndex] or item
+ end
+ end
+
+ return t.Items[#t.Items] or item
+ end,
+ --- Create child menu from properties of this object
+ ---@param t Menu|string MenuV menu
+ ---@param overrides table Properties to override in menu object (ignore parent)
+ ---@param namespace string Namespace of menu
+ InheritMenu = function(t, overrides, namespace)
+ return MenuV:InheritMenu(t, overrides, namespace)
+ end,
+ --- Add control key for specific menu
+ ---@param t Menu|string MenuV menu
+ ---@param action string Name of action
+ ---@param func function This will be executed
+ ---@param description string Key description
+ ---@param defaultType string Default key type
+ ---@param defaultKey string Default key
+ AddControlKey = function(t, action, func, description, defaultType, defaultKey)
+ if (U:Typeof(t.Namespace) ~= 'string' or t.Namespace == 'unknown') then
+ error('[MenuV] Namespace is required for assigning keys.')
+ return
+ end
+
+ MenuV:AddControlKey(t, action, func, description, defaultType, defaultKey)
+ end,
+ --- Assign key for opening this menu
+ ---@param t Menu|string MenuV menu
+ ---@param defaultType string Default key type
+ ---@param defaultKey string Default key
+ OpenWith = function(t, defaultType, defaultKey)
+ t:AddControlKey('open', function(m)
+ MenuV:CloseAll(function()
+ MenuV:OpenMenu(m)
+ end)
+ end, MenuV:T('open_menu'):format(t.Namespace), defaultType, defaultKey)
+ end,
+ --- Change title of menu
+ ---@param t Menu
+ ---@param title string Title of menu
+ SetTitle = function(t, title)
+ t.Title = U:Ensure(title, 'MenuV')
+ end,
+ --- Change subtitle of menu
+ ---@param t Menu
+ ---@param subtitle string Subtitle of menu
+ SetSubtitle = function(t, subtitle)
+ t.Subtitle = U:Ensure(subtitle, '')
+ end,
+ --- Change subtitle of menu
+ ---@param t Menu
+ ---@param position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
+ SetPosition = function(t, position)
+ t.Position = U:Ensure(position, 'topleft')
+ end,
+ --- Clear all Menu items
+ ---@param t Menu
+ ClearItems = function(t, update)
+ update = U:Ensure(update, true)
+
+ local items = CreateEmptyItemsTable({})
+
+ items(function(_, trigger, key, index, value, oldValue)
+ t:Trigger(trigger, key, index, value, oldValue)
+ end)
+
+ rawset(t.data, 'Items', items)
+
+ if (update and t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', 'Items', items)
+ end
+ end,
+ Open = function(t)
+ MenuV:OpenMenu(t)
+ end,
+ Close = function(t)
+ MenuV:CloseMenu(t)
+ end,
+ --- @see Menu to @see table
+ ---@param t Menu
+ ---@return table
+ ToTable = function(t)
+ local tempTable = {
+ theme = U:Ensure(t.Theme, 'default'),
+ uuid = U:Ensure(t.UUID, '00000000-0000-0000-0000-000000000000'),
+ title = U:Ensure(t.Title, 'MenuV'),
+ subtitle = U:Ensure(t.Subtitle, ''),
+ position = U:Ensure(t.Position, 'topleft'),
+ size = U:Ensure(t.Size, 'size-110'),
+ dictionary = U:Ensure(t.Dictionary, 'menuv'),
+ texture = U:Ensure(t.Texture, 'default'),
+ color = {
+ r = U:Ensure(t.Color.R, 0),
+ g = U:Ensure(t.Color.G, 0),
+ b = U:Ensure(t.Color.B, 255)
+ },
+ items = {}
+ }
+
+ local items = rawget(t.data, 'Items')
+
+ if (items ~= nil and items.ToTable ~= nil) then
+ tempTable.items = items:ToTable()
+ end
+
+ if (tempTable.color.r <= 0) then tempTable.color.r = 0 end
+ if (tempTable.color.r >= 255) then tempTable.color.r = 255 end
+ if (tempTable.color.g <= 0) then tempTable.color.g = 0 end
+ if (tempTable.color.g >= 255) then tempTable.color.g = 255 end
+ if (tempTable.color.b <= 0) then tempTable.color.b = 0 end
+ if (tempTable.color.b >= 255) then tempTable.color.b = 255 end
+
+ return tempTable
+ end
+ }
+
+ if (lower(item.Texture) == 'default' and lower(item.Dictionary) == 'menuv' and theme == 'native') then
+ item.Texture = 'default_native'
+ end
+
+ item.Events.OnOpen = {}
+ item.Events.OnClose = {}
+ item.Events.OnSelect = {}
+ item.Events.OnUpdate = {}
+ item.Events.OnSwitch = {}
+ item.Events.OnChange = {}
+ item.Events.OnIChange = {}
+
+ if (not U:Any(item.Size, { 'size-100', 'size-110', 'size-125', 'size-150', 'size-175', 'size-200' }, 'value')) then
+ item.Size = 'size-110'
+ end
+
+ local mt = {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ ---@param t Menu
+ __tostring = function(t)
+ return encode(t:ToTable())
+ end,
+ __call = function(t)
+ MenuV:OpenMenu(t)
+ end,
+ __newindex = function(t, k, v)
+ local whitelisted = { 'Title', 'Subtitle', 'Position', 'Color', 'R', 'G', 'B', 'Size', 'Dictionary', 'Texture', 'Theme' }
+ local key = U:Ensure(k, 'unknown')
+ local oldValue = rawget(t.data, k)
+
+ if (not U:Any(key, whitelisted, 'value') and oldValue ~= nil) then
+ return
+ end
+
+ local checkInput = t.Validate ~= nil and type(t.Validate) == 'function'
+ local inputParser = t.Parser ~= nil and type(t.Parser) == 'function'
+ local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function'
+
+ if (checkInput) then
+ local result = t:Validate(key, v)
+ result = U:Ensure(result, true)
+
+ if (not result) then
+ return
+ end
+ end
+
+ if (inputParser) then
+ local parsedValue = t:Parser(key, v)
+
+ v = parsedValue or v
+ end
+
+ rawset(t.data, k, v)
+
+ if (key == 'Theme' or key == 'theme') then
+ local theme_value = string.lower(U:Ensure(v, 'default'))
+
+ if (theme_value == 'native') then
+ rawset(t.data, 'color', { R = 255, G = 255, B = 255 })
+
+ local texture = U:Ensure(rawget(t.data, 'Texture'), 'default')
+
+ if (texture == 'default') then
+ rawset(t.data, 'Texture', 'default_native')
+ end
+ elseif (theme_value == 'default') then
+ local texture = U:Ensure(rawget(t.data, 'Texture'), 'default')
+
+ if (texture == 'default_native') then
+ rawset(t.data, 'Texture', 'default')
+ end
+ end
+ end
+
+ if (updateIndexTrigger) then
+ t:NewIndex(key, v)
+ end
+
+ if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
+ t:Trigger('update', key, v, oldValue)
+ end
+ end,
+ __len = function(t)
+ return #t.Items
+ end,
+ __pairs = function(t)
+ return pairs(rawget(t.data, 'Items') or {})
+ end,
+ __ipairs = function(t)
+ return ipairs(rawget(t.data, 'Items') or {})
+ end,
+ __metatable = 'MenuV',
+ }
+
+ ---@class Menu
+ ---@field public IsOpen boolean `true` if menu is open, otherwise `false`
+ ---@field public UUID string UUID of Menu
+ ---@field public Title string Title of Menu
+ ---@field public Subtitle string Subtitle of Menu
+ ---@field public Position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
+ ---@field public Texture string Name of texture example: "default"
+ ---@field public Dictionary string Name of dictionary example: "menuv"
+ ---@field public Color table Color of Menu
+ ---@field private Events table List of registered `on` events
+ ---@field public Items Item[] List of items
+ ---@field public Trigger fun(t: Item, event: string)
+ ---@field public On fun(t: Menu, event: string, func: function|Menu): string
+ ---@field public RemoveOnEvent fun(t: Menu, event: string, uuid: string)
+ ---@field public Validate fun(t: Menu, k: string, v:any)
+ ---@field public NewIndex fun(t: Menu, k: string, v: any)
+ ---@field public Parser fun(t: Menu, k: string, v: any)
+ ---@field public AddButton fun(t: Menu, info: table):Item
+ ---@field public AddCheckbox fun(t: Menu, info: table):Item
+ ---@field public AddSlider fun(t: Menu, info: table):SliderItem
+ ---@field public AddRange fun(t: Menu, info: table):RangeItem
+ ---@field public AddConfirm fun(t: Menu, info: table):ConfirmItem
+ ---@field public AddControlKey fun(t: Menu, action: string, func: function, description: string, defaultType: string, defaultKey: string)
+ ---@field public OpenWith fun(t: Menu, defaultType: string, defaultKey: string)
+ ---@field public SetTitle fun(t: Menu, title: string)
+ ---@field public SetSubtitle fun(t: Menu, subtitle: string)
+ ---@field public SetPosition fun(t: Menu, position: string)
+ ---@field public ClearItems fun(t: Menu)
+ ---@field public Open fun(t: Menu)
+ ---@field public Close fun(t: Menu)
+ ---@field public ToTable fun(t: Menu):table
+ local menu = setmetatable({ data = item, __class = 'Menu', __type = 'Menu' }, mt)
+
+ menu.Items(function(items, trigger, key, index, value, oldValue)
+ menu:Trigger(trigger, key, index, value, oldValue)
+ end)
+
+ for k, v in pairs(info or {}) do
+ local key = U:Ensure(k, 'unknown')
+
+ if (key == 'unknown') then return end
+
+ menu:On(key, v)
+ end
+
+ LoadTextureDictionary(menu.Dictionary)
+
+ return menu
+end
+
+_ENV.CreateMenu = CreateMenu
+_G.CreateMenu = CreateMenu
diff --git a/resources/[standalone]/menuv/menuv/components/translations.lua b/resources/[standalone]/menuv/menuv/components/translations.lua
new file mode 100644
index 0000000..4330710
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv/components/translations.lua
@@ -0,0 +1,35 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+local decode = assert(json.decode)
+
+--- FiveM globals
+local LoadResourceFile = assert(LoadResourceFile)
+
+--- MenuV globals
+---@type Utilities
+local Utilities = assert(Utilities)
+
+--- Empty translations table
+local translations = {}
+
+--- Load all translations
+local lang = Utilities:Ensure((Config or {}).Language, 'en')
+local translations_path = ('languages/%s.json'):format(lang)
+local translations_raw = LoadResourceFile('menuv', translations_path)
+
+if (translations_raw) then
+ local transFile = decode(translations_raw)
+
+ if (transFile) then translations = Utilities:Ensure(transFile.translations, {}) end
+end
+
+_ENV.translations = translations
+_G.translations = translations
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/menuv/components/utilities.lua b/resources/[standalone]/menuv/menuv/components/utilities.lua
new file mode 100644
index 0000000..bc84746
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv/components/utilities.lua
@@ -0,0 +1,427 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+local type = assert(type)
+local tonumber = assert(tonumber)
+local tostring = assert(tostring)
+local lower = assert(string.lower)
+local upper = assert(string.upper)
+local sub = assert(string.sub)
+local encode = assert(json.encode)
+local decode = assert(json.decode)
+local floor = assert(math.floor)
+local random = assert(math.random)
+local randomseed = assert(math.randomseed)
+local rawget = assert(rawget)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local GET_GAME_TIMER = assert(GetGameTimer)
+local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
+
+--- Utilities for MenuV
+---@class Utilities
+local Utilities = setmetatable({ __class = 'Utilities' }, {})
+
+--- Returns `true` if `input` starts with `start`, otherwise `false`
+---@param input string Checks if this string starts with `start`
+---@param start string Checks if `input` starts with this
+---@return boolean `true` if `input` starts with `start`, otherwise `false`
+function Utilities:StartsWith(input, start)
+ if (self:Typeof(input) ~= 'string') then return false end
+ if (self:Typeof(start) == 'number') then start = tostring(start) end
+ if (self:Typeof(start) ~= 'string') then return false end
+
+ return sub(input, 1, #start) == start
+end
+
+--- Returns `true` if `input` ends with `ends`, otherwise `false`
+---@param input string Checks if this string ends with `ends`
+---@param ends string Checks if `input` ends with this
+---@return boolean `true` if `input` ends with `ends`, otherwise `false`
+function Utilities:EndsWith(input, ends)
+ if (self:Typeof(input) ~= 'string') then return false end
+ if (self:Typeof(ends) == 'number') then ends = tostring(ends) end
+ if (self:Typeof(ends) ~= 'string') then return false end
+
+ return sub(input, -#ends) == ends
+end
+
+--- Returns the type of given `input`
+---@param input any Any input
+---@return string Type of given input
+function Utilities:Typeof(input)
+ if (input == nil) then return 'nil' end
+
+ local rawType = type(input) or 'nil'
+
+ if (rawType ~= 'table') then return rawType end
+
+ local isFXFunction = rawget(input, '__cfx_functionReference') ~= nil or
+ rawget(input, '__cfx_async_retval') ~= nil
+
+ if (isFXFunction) then return 'function' end
+ if (rawget(input, '__cfx_functionSource') ~= nil) then return 'number' end
+
+ local rawClass = rawget(input, '__class')
+
+ if (rawClass ~= nil) then return type(rawClass) == 'string' and rawClass or 'class' end
+
+ local rawTableType = rawget(input, '__type')
+
+ if (rawTableType ~= nil) then return type(rawTableType) == 'string' and rawTableType or 'table' end
+
+ return rawType
+end
+
+local INPUT_GROUPS = {
+ [0] = "KEYBOARD",
+ [2] = "CONTROLLER"
+}
+
+local INPUT_TYPE_GROUPS = {
+ ["KEYBOARD"] = 0,
+ ["MOUSE_ABSOLUTEAXIS"] = 0,
+ ["MOUSE_CENTEREDAXIS"] = 0,
+ ["MOUSE_RELATIVEAXIS"] = 0,
+ ["MOUSE_SCALEDAXIS"] = 0,
+ ["MOUSE_NORMALIZED"] = 0,
+ ["MOUSE_WHEEL"] = 0,
+ ["MOUSE_BUTTON"] = 0,
+ ["MOUSE_BUTTONANY"] = 0,
+ ["MKB_AXIS"] = 0,
+ ["PAD_AXIS"] = 2,
+ ["PAD_DIGITALBUTTON"] = 2,
+ ["PAD_DIGITALBUTTONANY"] = 2,
+ ["PAD_ANALOGBUTTON"] = 2,
+ ["JOYSTICK_POV"] = 2,
+ ["JOYSTICK_POV_AXIS"] = 2,
+ ["JOYSTICK_BUTTON"] = 2,
+ ["JOYSTICK_AXIS"] = 2,
+ ["JOYSTICK_IAXIS"] = 2,
+ ["JOYSTICK_AXIS_NEGATIVE"] = 2,
+ ["JOYSTICK_AXIS_POSITIVE"] = 2,
+ ["PAD_DEBUGBUTTON"] = 2,
+ ["GAME_CONTROLLED"] = 2,
+ ["DIGITALBUTTON_AXIS"] = 2,
+}
+
+function Utilities:GetInputTypeGroup(inputType)
+ return INPUT_TYPE_GROUPS[inputType] or 0
+end
+
+function Utilities:GetInputGroupName(inputTypeGroup)
+ return INPUT_GROUPS[inputTypeGroup] or "KEYBOARD"
+end
+
+--- Transform any `input` to the same type as `defaultValue`
+---@type function
+---@param input any Transform this `input` to `defaultValue`'s type
+---@param defaultValue any Returns this if `input` can't transformed to this type
+---@param ignoreDefault boolean Don't return default value if this is true
+---@return any Returns `input` matches the `defaultValue` type or `defaultValue`
+function Utilities:Ensure(input, defaultValue, ignoreDefault)
+ ignoreDefault = type(ignoreDefault) == 'boolean' and ignoreDefault or false
+
+ if (defaultValue == nil) then return nil end
+
+ local requiredType = self:Typeof(defaultValue)
+
+ if (requiredType == 'nil') then return nil end
+
+ local inputType = self:Typeof(input)
+
+ if (inputType == requiredType) then return input end
+ if (inputType == 'nil') then return defaultValue end
+
+ if (requiredType == 'number') then
+ if (inputType == 'boolean') then return input and 1 or 0 end
+
+ return tonumber(input) or (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'string') then
+ if (inputType == 'boolean') then return input and 'yes' or 'no' end
+ if (inputType == 'vector3') then return encode({ x = input.x, y = input.y, z = input.z }) or (not ignoreDefault and defaultValue or nil) end
+ if (inputType == 'vector2') then return encode({ x = input.x, y = input.y }) or (not ignoreDefault and defaultValue or nil) end
+ if (inputType == 'table') then return encode(input) or (not ignoreDefault and defaultValue or nil) end
+
+ local result = tostring(input)
+
+ if (result == 'nil') then
+ return not ignoreDefault and defaultValue or 'nil'
+ end
+
+ return result
+ end
+
+ if (requiredType == 'boolean') then
+ if (inputType == 'string') then
+ input = lower(input)
+
+ if (input == 'true' or input == '1' or input == 'yes' or input == 'y') then return true end
+ if (input == 'false' or input == '0' or input == 'no' or input == 'n') then return false end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (inputType == 'number') then
+ if (input == 1) then return true end
+ if (input == 0) then return false end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'table') then
+ if (inputType == 'string') then
+ if (self:StartsWith(input, '{') and self:EndsWith(input, '}')) then
+ return decode(input) or (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (self:StartsWith(input, '[') and self:EndsWith(input, ']')) then
+ return decode(input) or (not ignoreDefault and defaultValue or nil)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (inputType == 'vector3') then return { x = input.x or 0, y = input.y or 0, z = input.z or 0 } end
+ if (inputType == 'vector2') then return { x = input.x or 0, y = input.y or 0 } end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'vector3') then
+ if (inputType == 'table') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+ local _z = self:Ensure(input.z, defaultValue.z)
+
+ return vector3(_x, _y, _z)
+ end
+
+ if (inputType == 'vector2') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+
+ return vector3(_x, _y, 0)
+ end
+
+ if (inputType == 'number') then
+ return vector3(input, input, input)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ if (requiredType == 'vector2') then
+ if (inputType == 'table') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+
+ return vector2(_x, _y)
+ end
+
+ if (inputType == 'vector3') then
+ local _x = self:Ensure(input.x, defaultValue.x)
+ local _y = self:Ensure(input.y, defaultValue.y)
+
+ return vector2(_x, _y)
+ end
+
+ if (inputType == 'number') then
+ return vector2(input, input)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+ end
+
+ return (not ignoreDefault and defaultValue or nil)
+end
+
+--- Checks if input exists in inputs
+--- '0' and 0 are both the same '0' == 0 equals `true`
+--- 'yes' and true are both the same 'yes' == true equals `true`
+---@param input any Any input
+---@param inputs any[] Any table
+---@param checkType string | "'value'" | "'key'" | "'both'"
+---@return boolean Returns `true` if input has been found as `key` and/or `value`
+function Utilities:Any(input, inputs, checkType)
+ if (input == nil) then return false end
+ if (inputs == nil) then return false end
+
+ inputs = self:Ensure(inputs, {})
+ checkType = lower(self:Ensure(checkType, 'value'))
+
+ local checkMethod = 1
+
+ if (checkType == 'value' or checkType == 'v') then
+ checkMethod = 1
+ elseif (checkType == 'key' or checkType == 'k') then
+ checkMethod = -1
+ elseif (checkType == 'both' or checkType == 'b') then
+ checkMethod = 0
+ end
+
+ for k, v in pairs(inputs) do
+ if (checkMethod == 0 or checkMethod == -1) then
+ local checkK = self:Ensure(input, k, true)
+
+ if (checkK ~= nil and checkK == k) then return true end
+ end
+
+ if (checkMethod == 0 or checkMethod == 1) then
+ local checkV = self:Ensure(input, v, true)
+
+ if (checkV ~= nil and checkV == v) then return true end
+ end
+ end
+
+ return false
+end
+
+--- Round any `value`
+---@param value number Round this value
+---@param decimal number Number of decimals
+---@return number Rounded number
+function Utilities:Round(value, decimal)
+ value = self:Ensure(value, 0)
+ decimal = self:Ensure(decimal, 0)
+
+ if (decimal > 0) then
+ return floor((value * 10 ^ decimal) + 0.5) / (10 ^ decimal)
+ end
+
+ return floor(value + 0.5)
+end
+
+--- Checks if `item1` equals `item2`
+---@param item1 any Item1
+---@param item2 any Item2
+---@return boolean `true` if both are equal, otherwise `false`
+function Utilities:Equal(item1, item2)
+ if (item1 == nil and item2 == nil) then return true end
+ if (item1 == nil or item2 == nil) then return false end
+
+ if (type(item1) == 'table') then
+ local item1EQ = rawget(item1, '__eq')
+
+ if (item1EQ ~= nil and self:Typeof(item1EQ) == 'function') then
+ return item1EQ(item1, item2)
+ end
+
+ return item1 == item2
+ end
+
+ if (type(item2) == 'table') then
+ local item2EQ = rawget(item2, '__eq')
+
+ if (item2EQ ~= nil and self:Typeof(item2EQ) == 'function') then
+ return item2EQ(item2, item1)
+ end
+
+ return item2 == item1
+ end
+
+ return item1 == item2
+end
+
+local function tohex(x)
+ x = Utilities:Ensure(x, 32)
+
+ local s, base, d = '', 16
+
+ while x > 0 do
+ d = x % base + 1
+ x = floor(x / base)
+ s = sub('0123456789abcdef', d, d) .. s
+ end
+
+ while #s < 2 do s = ('0%s'):format(s) end
+
+ return s
+end
+
+local function bitwise(x, y, matrix)
+ x = Utilities:Ensure(x, 32)
+ y = Utilities:Ensure(y, 16)
+ matrix = Utilities:Ensure(matrix, {{0,0}, {0, 1}})
+
+ local z, pow = 0, 1
+
+ while x > 0 or y > 0 do
+ z = z + (matrix[x %2 + 1][y %2 + 1] * pow)
+ pow = pow * 2
+ x = floor(x / 2)
+ y = floor(y / 2)
+ end
+
+ return z
+end
+
+--- Generates a random UUID like: 00000000-0000-0000-0000-000000000000
+---@return string Random generated UUID
+function Utilities:UUID()
+ randomseed(GET_GAME_TIMER() + random(30720, 92160))
+
+ ---@type number[]
+ local bytes = {
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255),
+ random(0, 255)
+ }
+
+ bytes[7] = bitwise(bytes[7], 0x0f, {{0,0},{0,1}})
+ bytes[7] = bitwise(bytes[7], 0x40, {{0,1},{1,1}})
+ bytes[9] = bitwise(bytes[7], 0x3f, {{0,0},{0,1}})
+ bytes[9] = bitwise(bytes[7], 0x80, {{0,1},{1,1}})
+
+ return upper(('%s%s%s%s-%s%s-%s%s-%s%s-%s%s%s%s%s%s'):format(
+ tohex(bytes[1]), tohex(bytes[2]), tohex(bytes[3]), tohex(bytes[4]),
+ tohex(bytes[5]), tohex(bytes[6]),
+ tohex(bytes[7]), tohex(bytes[8]),
+ tohex(bytes[9]), tohex(bytes[10]),
+ tohex(bytes[11]), tohex(bytes[12]), tohex(bytes[13]), tohex(bytes[14]), tohex(bytes[15]), tohex(bytes[16])
+ ))
+end
+
+--- Replace a string that contains `this` to `that`
+---@param str string String where to replace in
+---@param this string Word that's need to be replaced
+---@param that string Replace `this` whit given string
+---@return string String where `this` has been replaced with `that`
+function Utilities:Replace(str, this, that)
+ local b, e = str:find(this, 1, true)
+
+ if b == nil then
+ return str
+ else
+ return str:sub(1, b - 1) .. that .. self:Replace(str:sub(e + 1), this, that)
+ end
+end
+
+_G.Utilities = Utilities
+_ENV.Utilities = Utilities
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/menuv/menuv.lua b/resources/[standalone]/menuv/menuv/menuv.lua
new file mode 100644
index 0000000..968dafb
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv/menuv.lua
@@ -0,0 +1,293 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu library for creating menu's
+----------------------- [ MenuV ] -----------------------
+local assert = assert
+local load = assert(load)
+local xpcall = assert(xpcall)
+local lower = assert(string.lower)
+local upper = assert(string.upper)
+local rawget = assert(rawget)
+local rawset = assert(rawset)
+local traceback = assert(debug.traceback)
+local setmetatable = assert(setmetatable)
+
+--- FiveM globals
+local GetInvokingResource = assert(GetInvokingResource)
+local LoadResourceFile = assert(LoadResourceFile)
+local RegisterKeyMapping = assert(RegisterKeyMapping)
+local RegisterCommand = assert(RegisterCommand)
+local SendNUIMessage = assert(SendNUIMessage)
+local RegisterNUICallback = assert(RegisterNUICallback)
+local IsScreenFadedOut = assert(IsScreenFadedOut)
+local IsPauseMenuActive = assert(IsPauseMenuActive)
+local PlaySoundFrontend = assert(PlaySoundFrontend)
+local CreateThread = assert(Citizen.CreateThread)
+local Wait = assert(Citizen.Wait)
+local exports = assert(exports)
+
+--- MenuV globals
+---@type Utilities
+local Utilities = assert(Utilities)
+
+--- Load a file from `menuv`
+---@param path string Path in `menuv`
+---@return any|nil Results of nil
+local function load_file(path)
+ if (path == nil or type(path) ~= 'string') then return nil end
+
+ local raw_file = LoadResourceFile('menuv', path)
+
+ if (raw_file) then
+ local raw_func, _ = load(raw_file, ('menuv/%s'):format(path), 't', _ENV)
+
+ if (raw_func) then
+ local ok, result = xpcall(raw_func, traceback)
+
+ if (ok) then
+ return result
+ end
+ end
+ end
+
+ return nil
+end
+
+load_file('menuv/components/translations.lua')
+
+local MenuV = setmetatable({
+ ---@type string
+ __class = 'MenuV',
+ ---@type string
+ __type = 'MenuV',
+ ---@type boolean
+ Loaded = false,
+ ---@type number
+ ThreadWait = Utilities:Ensure((Config or {}).HideInterval, 250),
+ ---@type table
+ Translations = translations or {},
+ ---@type table
+ Sounds = Utilities:Ensure((Config or {}).Sounds, {}),
+ ---@type boolean
+ Hidden = false
+}, {})
+
+MenuV.Keys = setmetatable({ data = {}, __class = 'MenuVKeys', __type = 'keys' }, {
+ __index = function(t, k)
+ return rawget(t.data, k)
+ end,
+ __newindex = function(t, k, v)
+ k = Utilities:Ensure(k, 'unknown')
+
+ if (k == 'unknown') then return end
+
+ local rawKey = rawget(t.data, k)
+ local keyExists = rawKey ~= nil
+ local prevState = Utilities:Ensure((rawKey or {}).status, false)
+ local newState = Utilities:Ensure(v, false)
+
+ if (keyExists and not MenuV.Hidden) then
+ rawset(t.data[k], 'status', newState)
+
+ if (prevState ~= newState) then
+ local action = newState and not prevState and 'KEY_PRESSED' or 'KEY_RELEASED'
+ local key = Utilities:Ensure(rawKey.action, 'UNKNOWN')
+
+ SendNUIMessage({ action = action, key = key })
+ end
+ end
+ end,
+ __call = function(t, k, a, inputType)
+ k = Utilities:Ensure(k, 'unknown')
+ a = Utilities:Ensure(a, 'UNKNOWN')
+ inputType = Utilities:Ensure(inputType, 0)
+
+ if (k == 'unknown') then return end
+
+ local rawKey = rawget(t.data, k)
+ local keyExists = rawKey ~= nil
+
+ if (keyExists) then
+ if not rawKey.inputTypes[inputType] then
+ rawKey.inputTypes[inputType] = true
+ end
+
+ return
+ end
+
+ rawset(t.data, k, { status = false, action = a, inputTypes = { [inputType] = true } })
+ end
+})
+
+--- Register a `action` with custom keybind
+---@param action string Action like: UP, DOWN, LEFT...
+---@param description string Description of keybind
+---@param defaultType string Type like: keyboard, mouse etc.
+---@param defaultKey string Default key for this keybind
+function MenuV:RegisterKey(action, description, defaultType, defaultKey)
+ action = Utilities:Ensure(action, 'UNKNOWN')
+ description = Utilities:Ensure(description, 'unknown')
+ defaultType = Utilities:Ensure(defaultType, 'KEYBOARD')
+ defaultType = upper(defaultType)
+ defaultKey = Utilities:Ensure(defaultKey, 'F12')
+
+ action = Utilities:Replace(action, ' ', '_')
+ action = upper(action)
+
+ local typeGroup = Utilities:GetInputTypeGroup(defaultType)
+
+ if (self.Keys[action] and self.Keys[action].inputTypes[typeGroup]) then return end
+
+ self.Keys(action, action, typeGroup)
+
+ local k = lower(action)
+
+ if typeGroup > 0 then
+ local inputGroupName = Utilities:GetInputGroupName(typeGroup)
+ k = ('%s_%s'):format(lower(inputGroupName), k)
+ end
+
+ k = ('menuv_%s'):format(k)
+
+ RegisterKeyMapping(('+%s'):format(k), description, defaultType, defaultKey)
+ RegisterCommand(('+%s'):format(k), function() MenuV.Keys[action] = true end)
+ RegisterCommand(('-%s'):format(k), function() MenuV.Keys[action] = false end)
+end
+
+--- Load translation
+---@param k string Translation key
+---@return string Translation or 'MISSING TRANSLATION'
+local function T(k)
+ k = Utilities:Ensure(k, 'unknown')
+
+ return Utilities:Ensure(MenuV.Translations[k], 'MISSING TRANSLATION')
+end
+
+RegisterNUICallback('loaded', function(_, cb)
+ MenuV.Loaded = true
+ cb('ok')
+end)
+
+RegisterNUICallback('sound', function(info, cb)
+ local key = upper(Utilities:Ensure(info.key, 'UNKNOWN'))
+
+ if (MenuV.Sounds == nil and MenuV.Sounds[key] == nil) then cb('ok') return end
+
+ local sound = Utilities:Ensure(MenuV.Sounds[key], {})
+ local soundType = lower(Utilities:Ensure(sound.type, 'unknown'))
+
+ if (soundType == 'native') then
+ local name = Utilities:Ensure(sound.name, 'UNKNOWN')
+ local library = Utilities:Ensure(sound.library, 'UNKNOWN')
+
+ PlaySoundFrontend(-1, name, library, true)
+ end
+
+ cb('ok')
+end)
+
+--- Trigger the NUICallback for the right resource
+---@param name string Name of callback
+---@param info table Info returns from callback
+---@param cb function Trigger this when callback is done
+local function TriggerResourceCallback(name, info, cb)
+ local r = Utilities:Ensure(info.r, 'menuv')
+
+ if (r == 'menuv') then cb('ok') return end
+
+ local resource = exports[r] or nil
+
+ if (resource == nil) then cb('ok') return end
+
+ local nuiCallback = resource['NUICallback'] or nil
+
+ if (nuiCallback == nil) then cb('ok') return end
+
+ exports[r]:NUICallback(name, info, cb)
+end
+
+RegisterNUICallback('submit', function(info, cb) TriggerResourceCallback('submit', info, cb) end)
+RegisterNUICallback('close', function(info, cb) TriggerResourceCallback('close', info, cb) end)
+RegisterNUICallback('switch', function(info, cb) TriggerResourceCallback('switch', info, cb) end)
+RegisterNUICallback('update', function(info, cb) TriggerResourceCallback('update', info, cb) end)
+RegisterNUICallback('open', function(info, cb) TriggerResourceCallback('open', info, cb) end)
+RegisterNUICallback('opened', function(info, cb) TriggerResourceCallback('opened', info, cb) end)
+RegisterNUICallback('close_all', function(info, cb) TriggerResourceCallback('close_all', info, cb) end)
+
+--- MenuV exports
+exports('IsLoaded', function(cb)
+ cb = Utilities:Ensure(cb, function() end)
+
+ if (MenuV.Loaded) then
+ cb()
+ return
+ end
+
+ CreateThread(function()
+ local callback = cb
+
+ repeat Wait(0) until MenuV.Loaded
+
+ callback()
+ end)
+end)
+
+exports('SendNUIMessage', function(input)
+ local r = Utilities:Ensure(GetInvokingResource(), 'menuv')
+
+ if (Utilities:Typeof(input) == 'table') then
+ if (input.menu) then
+ rawset(input.menu, 'resource', r)
+ rawset(input.menu, 'defaultSounds', MenuV.Sounds)
+ rawset(input.menu, 'hidden', MenuV.Hidden)
+ end
+
+ SendNUIMessage(input)
+ end
+end)
+
+--- Register `MenuV` keybinds
+MenuV:RegisterKey('UP', T('keybind_key_up'), 'KEYBOARD', 'UP')
+MenuV:RegisterKey('DOWN', T('keybind_key_down'), 'KEYBOARD', 'DOWN')
+MenuV:RegisterKey('LEFT', T('keybind_key_left'), 'KEYBOARD', 'LEFT')
+MenuV:RegisterKey('RIGHT', T('keybind_key_right'), 'KEYBOARD', 'RIGHT')
+MenuV:RegisterKey('ENTER', T('keybind_key_enter'), 'KEYBOARD', 'RETURN')
+MenuV:RegisterKey('CLOSE', T('keybind_key_close'), 'KEYBOARD', 'BACK')
+MenuV:RegisterKey('CLOSE_ALL', T('keybind_key_close_all'), 'KEYBOARD', 'PLUS')
+
+MenuV:RegisterKey('UP', ('%s - %s'):format(T('controller'), T('keybind_key_up')), 'PAD_ANALOGBUTTON', 'LUP_INDEX')
+MenuV:RegisterKey('DOWN', ('%s - %s'):format(T('controller'), T('keybind_key_down')), 'PAD_ANALOGBUTTON', 'LDOWN_INDEX')
+MenuV:RegisterKey('LEFT', ('%s - %s'):format(T('controller'), T('keybind_key_left')), 'PAD_ANALOGBUTTON', 'LLEFT_INDEX')
+MenuV:RegisterKey('RIGHT', ('%s - %s'):format(T('controller'), T('keybind_key_right')), 'PAD_ANALOGBUTTON', 'LRIGHT_INDEX')
+MenuV:RegisterKey('ENTER', ('%s - %s'):format(T('controller'), T('keybind_key_enter')), 'PAD_ANALOGBUTTON', 'RDOWN_INDEX')
+MenuV:RegisterKey('CLOSE', ('%s - %s'):format(T('controller'), T('keybind_key_close')), 'PAD_ANALOGBUTTON', 'RRIGHT_INDEX')
+MenuV:RegisterKey('CLOSE_ALL', ('%s - %s'):format(T('controller'), T('keybind_key_close_all')), 'PAD_ANALOGBUTTON', 'R3_INDEX')
+
+--- Hide menu when screen is faded out or pause menu ia active
+CreateThread(function()
+ MenuV.Hidden = false
+
+ while true do
+ repeat Wait(0) until MenuV.Loaded
+
+ local new_state = IsScreenFadedOut() or IsPauseMenuActive()
+
+ if (MenuV.Hidden ~= new_state) then
+ SendNUIMessage({ action = 'UPDATE_STATUS', status = not new_state })
+ end
+
+ MenuV.Hidden = new_state
+
+ Wait(MenuV.ThreadWait)
+ end
+end)
+
+--- When resource is stopped
+AddEventHandler('onResourceStop', function(resourceName)
+ SendNUIMessage({ action = 'RESOURCE_STOPPED', resource = resourceName })
+end)
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/menuv_example/example.lua b/resources/[standalone]/menuv/menuv_example/example.lua
new file mode 100644
index 0000000..1d5cf6f
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv_example/example.lua
@@ -0,0 +1,45 @@
+--- MenuV Menu
+---@type Menu
+local menu = MenuV:CreateMenu(false, 'Welcome to MenuV', 'topleft', 255, 0, 0, 'size-125', 'example', 'menuv', 'example_namespace')
+local menu2 = MenuV:CreateMenu('Demo 2', 'Open this demo menu in MenuV', 'topleft', 255, 0, 0)
+
+local menu_button = menu:AddButton({ icon = '😃', label = 'Open Demo 2 Menu', value = menu2, description = 'YEA :D from first menu' })
+local menu2_button = menu2:AddButton({ icon = '😃', label = 'Open First Menu', value = menu, description = 'YEA :D from second menu' })
+local confirm = menu:AddConfirm({ icon = '🔥', label = 'Confirm', value = 'no' })
+local range = menu:AddRange({ icon = '⚽', label = 'Range Item', min = 0, max = 10, value = 0, saveOnUpdate = true })
+local checkbox = menu:AddCheckbox({ icon = '💡', label = 'Checkbox Item', value = 'n' })
+local checkbox_disabled = menu:AddCheckbox({ icon = '💡', label = 'Checkbox Disabled', value = 'n', disabled = true })
+local slider = menu:AddSlider({ icon = '❤️', label = 'Slider', value = 'demo', values = {
+ { label = 'Demo Item', value = 'demo', description = 'Demo Item 1' },
+ { label = 'Demo Item 2', value = 'demo2', description = 'Demo Item 2' },
+ { label = 'Demo Item 3', value = 'demo3', description = 'Demo Item 3' },
+ { label = 'Demo Item 4', value = 'demo4', description = 'Demo Item 4' }
+}})
+
+--- Events
+confirm:On('confirm', function(item) print('YOU ACCEPTED THE TERMS') end)
+confirm:On('deny', function(item) print('YOU DENIED THE TERMS') end)
+
+range:On('select', function(item, value) print(('FROM %s to %s YOU SELECTED %s'):format(item.Min, item.Max, value)) end)
+range:On('change', function(item, newValue, oldValue)
+ menu.Title = ('MenuV %s'):format(newValue)
+end)
+
+slider:On('select', function(item, value) print(('YOU SELECTED %s'):format(value)) end)
+
+confirm:On('enter', function(item) print('YOU HAVE NOW A CONFIRM ACTIVE') end)
+confirm:On('leave', function(item) print('YOU LEFT OUR CONFIRM :(') end)
+
+menu:On('switch', function(item, currentItem, prevItem) print(('YOU HAVE SWITCH THE ITEMS FROM %s TO %s'):format(prevItem.__type, currentItem.__type)) end)
+
+menu2:On('open', function(m)
+ m:ClearItems()
+
+ for i = 1, 10, 1 do
+ math.randomseed(GetGameTimer() + i)
+
+ m:AddButton({ ignoreUpdate = i ~= 10, icon = '❤️', label = ('Open Menu %s'):format(math.random(0, 1000)), value = menu, description = ('YEA! ANOTHER RANDOM NUMBER: %s'):format(math.random(0, 1000)), select = function(i) print('YOU CLICKED ON THIS ITEM!!!!') end })
+ end
+end)
+
+menu:OpenWith('KEYBOARD', 'F1') -- Press F1 to open Menu
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/menuv_example/fxmanifest.lua b/resources/[standalone]/menuv/menuv_example/fxmanifest.lua
new file mode 100644
index 0000000..0946e13
--- /dev/null
+++ b/resources/[standalone]/menuv/menuv_example/fxmanifest.lua
@@ -0,0 +1,28 @@
+----------------------- [ MenuV ] -----------------------
+-- GitHub: https://github.com/ThymonA/menuv/
+-- License: GNU General Public License v3.0
+-- https://choosealicense.com/licenses/gpl-3.0/
+-- Author: Thymon Arens
+-- Name: MenuV
+-- Version: 1.4.1
+-- Description: FiveM menu libarary for creating menu's
+----------------------- [ MenuV ] -----------------------
+
+fx_version 'cerulean'
+game 'gta5'
+
+name 'MenuV'
+version '1.4.1'
+description 'FiveM menu libarary for creating menu\'s'
+author 'ThymonA'
+contact 'contact@arens.io'
+url 'https://github.com/ThymonA/menuv/'
+
+client_scripts {
+ '@menuv/menuv.lua',
+ 'example.lua'
+}
+
+dependencies {
+ 'menuv'
+}
\ No newline at end of file
diff --git a/resources/[standalone]/menuv/stream/menuv.ytd b/resources/[standalone]/menuv/stream/menuv.ytd
new file mode 100644
index 0000000..06d2039
Binary files /dev/null and b/resources/[standalone]/menuv/stream/menuv.ytd differ
diff --git a/resources/[standalone]/menuv/templates/default.png b/resources/[standalone]/menuv/templates/default.png
new file mode 100644
index 0000000..ff207ab
Binary files /dev/null and b/resources/[standalone]/menuv/templates/default.png differ
diff --git a/resources/[standalone]/menuv/templates/default_native.png b/resources/[standalone]/menuv/templates/default_native.png
new file mode 100644
index 0000000..9b23d8e
Binary files /dev/null and b/resources/[standalone]/menuv/templates/default_native.png differ
diff --git a/resources/[standalone]/menuv/templates/example.png b/resources/[standalone]/menuv/templates/example.png
new file mode 100644
index 0000000..6946743
Binary files /dev/null and b/resources/[standalone]/menuv/templates/example.png differ
diff --git a/resources/[standalone]/menuv/templates/menuv.ytd b/resources/[standalone]/menuv/templates/menuv.ytd
new file mode 100644
index 0000000..06d2039
Binary files /dev/null and b/resources/[standalone]/menuv/templates/menuv.ytd differ
diff --git a/resources/[standalone]/menuv/templates/template.psd b/resources/[standalone]/menuv/templates/template.psd
new file mode 100644
index 0000000..9fe1e62
Binary files /dev/null and b/resources/[standalone]/menuv/templates/template.psd differ