this repo has no description

Merge branch 'develop' into nix

+3 -4
CHANGELOG.md
···
-
- Add `spacepack.filterReal`
-
- More dependency resolver stages
-
- Injector now supports "other patch methods"
-
- OpenAsar fixes
+
- Added multiline string input for extension settings
+
- Fixed conflicting extensions in Moonbase
+
- Fixes for latest Discord
+125 -621
LICENSE
···
-
GNU AFFERO GENERAL PUBLIC LICENSE
-
Version 3, 19 November 2007
+
GNU LESSER GENERAL PUBLIC LICENSE
+
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
-
Preamble
-
The GNU Affero General Public License is a free, copyleft license for
-
software and other kinds of works, specifically designed to ensure
-
cooperation with the community in the case of network server software.
-
-
The licenses for most software and other practical works are designed
-
to take away your freedom to share and change the works. By contrast,
-
our General Public Licenses are intended to guarantee your freedom to
-
share and change all versions of a program--to make sure it remains free
-
software for all its users.
-
-
When we speak of free software, we are referring to freedom, not
-
price. Our General Public Licenses are designed to make sure that you
-
have the freedom to distribute copies of free software (and charge for
-
them if you wish), that you receive source code or can get it if you
-
want it, that you can change the software or use pieces of it in new
-
free programs, and that you know you can do these things.
-
-
Developers that use our General Public Licenses protect your rights
-
with two steps: (1) assert copyright on the software, and (2) offer
-
you this License which gives you legal permission to copy, distribute
-
and/or modify the software.
-
-
A secondary benefit of defending all users' freedom is that
-
improvements made in alternate versions of the program, if they
-
receive widespread use, become available for other developers to
-
incorporate. Many developers of free software are heartened and
-
encouraged by the resulting cooperation. However, in the case of
-
software used on network servers, this result may fail to come about.
-
The GNU General Public License permits making a modified version and
-
letting the public access it on a server without ever releasing its
-
source code to the public.
-
-
The GNU Affero General Public License is designed specifically to
-
ensure that, in such cases, the modified source code becomes available
-
to the community. It requires the operator of a network server to
-
provide the source code of the modified version running there to the
-
users of that server. Therefore, public use of a modified version, on
-
a publicly accessible server, gives the public access to the source
-
code of the modified version.
-
-
An older license, called the Affero General Public License and
-
published by Affero, was designed to accomplish similar goals. This is
-
a different license, not a version of the Affero GPL, but Affero has
-
released a new version of the Affero GPL which permits relicensing under
-
this license.
-
-
The precise terms and conditions for copying, distribution and
-
modification follow.
-
-
TERMS AND CONDITIONS
-
-
0. Definitions.
-
-
"This License" refers to version 3 of the GNU Affero General Public License.
-
-
"Copyright" also means copyright-like laws that apply to other kinds of
-
works, such as semiconductor masks.
-
-
"The Program" refers to any copyrightable work licensed under this
-
License. Each licensee is addressed as "you". "Licensees" and
-
"recipients" may be individuals or organizations.
-
-
To "modify" a work means to copy from or adapt all or part of the work
-
in a fashion requiring copyright permission, other than the making of an
-
exact copy. The resulting work is called a "modified version" of the
-
earlier work or a work "based on" the earlier work.
-
-
A "covered work" means either the unmodified Program or a work based
-
on the Program.
-
-
To "propagate" a work means to do anything with it that, without
-
permission, would make you directly or secondarily liable for
-
infringement under applicable copyright law, except executing it on a
-
computer or modifying a private copy. Propagation includes copying,
-
distribution (with or without modification), making available to the
-
public, and in some countries other activities as well.
-
-
To "convey" a work means any kind of propagation that enables other
-
parties to make or receive copies. Mere interaction with a user through
-
a computer network, with no transfer of a copy, is not conveying.
-
-
An interactive user interface displays "Appropriate Legal Notices"
-
to the extent that it includes a convenient and prominently visible
-
feature that (1) displays an appropriate copyright notice, and (2)
-
tells the user that there is no warranty for the work (except to the
-
extent that warranties are provided), that licensees may convey the
-
work under this License, and how to view a copy of this License. If
-
the interface presents a list of user commands or options, such as a
-
menu, a prominent item in the list meets this criterion.
-
-
1. Source Code.
-
-
The "source code" for a work means the preferred form of the work
-
for making modifications to it. "Object code" means any non-source
-
form of a work.
-
-
A "Standard Interface" means an interface that either is an official
-
standard defined by a recognized standards body, or, in the case of
-
interfaces specified for a particular programming language, one that
-
is widely used among developers working in that language.
-
-
The "System Libraries" of an executable work include anything, other
-
than the work as a whole, that (a) is included in the normal form of
-
packaging a Major Component, but which is not part of that Major
-
Component, and (b) serves only to enable use of the work with that
-
Major Component, or to implement a Standard Interface for which an
-
implementation is available to the public in source code form. A
-
"Major Component", in this context, means a major essential component
-
(kernel, window system, and so on) of the specific operating system
-
(if any) on which the executable work runs, or a compiler used to
-
produce the work, or an object code interpreter used to run it.
-
-
The "Corresponding Source" for a work in object code form means all
-
the source code needed to generate, install, and (for an executable
-
work) run the object code and to modify the work, including scripts to
-
control those activities. However, it does not include the work's
-
System Libraries, or general-purpose tools or generally available free
-
programs which are used unmodified in performing those activities but
-
which are not part of the work. For example, Corresponding Source
-
includes interface definition files associated with source files for
-
the work, and the source code for shared libraries and dynamically
-
linked subprograms that the work is specifically designed to require,
-
such as by intimate data communication or control flow between those
-
subprograms and other parts of the work.
-
-
The Corresponding Source need not include anything that users
-
can regenerate automatically from other parts of the Corresponding
-
Source.
-
-
The Corresponding Source for a work in source code form is that
-
same work.
-
-
2. Basic Permissions.
-
-
All rights granted under this License are granted for the term of
-
copyright on the Program, and are irrevocable provided the stated
-
conditions are met. This License explicitly affirms your unlimited
-
permission to run the unmodified Program. The output from running a
-
covered work is covered by this License only if the output, given its
-
content, constitutes a covered work. This License acknowledges your
-
rights of fair use or other equivalent, as provided by copyright law.
-
-
You may make, run and propagate covered works that you do not
-
convey, without conditions so long as your license otherwise remains
-
in force. You may convey covered works to others for the sole purpose
-
of having them make modifications exclusively for you, or provide you
-
with facilities for running those works, provided that you comply with
-
the terms of this License in conveying all material for which you do
-
not control copyright. Those thus making or running the covered works
-
for you must do so exclusively on your behalf, under your direction
-
and control, on terms that prohibit them from making any copies of
-
your copyrighted material outside their relationship with you.
-
-
Conveying under any other circumstances is permitted solely under
-
the conditions stated below. Sublicensing is not allowed; section 10
-
makes it unnecessary.
-
-
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-
No covered work shall be deemed part of an effective technological
-
measure under any applicable law fulfilling obligations under article
-
11 of the WIPO copyright treaty adopted on 20 December 1996, or
-
similar laws prohibiting or restricting circumvention of such
-
measures.
-
-
When you convey a covered work, you waive any legal power to forbid
-
circumvention of technological measures to the extent such circumvention
-
is effected by exercising rights under this License with respect to
-
the covered work, and you disclaim any intention to limit operation or
-
modification of the work as a means of enforcing, against the work's
-
users, your or third parties' legal rights to forbid circumvention of
-
technological measures.
-
-
4. Conveying Verbatim Copies.
-
-
You may convey verbatim copies of the Program's source code as you
-
receive it, in any medium, provided that you conspicuously and
-
appropriately publish on each copy an appropriate copyright notice;
-
keep intact all notices stating that this License and any
-
non-permissive terms added in accord with section 7 apply to the code;
-
keep intact all notices of the absence of any warranty; and give all
-
recipients a copy of this License along with the Program.
-
-
You may charge any price or no price for each copy that you convey,
-
and you may offer support or warranty protection for a fee.
-
-
5. Conveying Modified Source Versions.
-
-
You may convey a work based on the Program, or the modifications to
-
produce it from the Program, in the form of source code under the
-
terms of section 4, provided that you also meet all of these conditions:
-
-
a) The work must carry prominent notices stating that you modified
-
it, and giving a relevant date.
-
-
b) The work must carry prominent notices stating that it is
-
released under this License and any conditions added under section
-
7. This requirement modifies the requirement in section 4 to
-
"keep intact all notices".
-
-
c) You must license the entire work, as a whole, under this
-
License to anyone who comes into possession of a copy. This
-
License will therefore apply, along with any applicable section 7
-
additional terms, to the whole of the work, and all its parts,
-
regardless of how they are packaged. This License gives no
-
permission to license the work in any other way, but it does not
-
invalidate such permission if you have separately received it.
-
-
d) If the work has interactive user interfaces, each must display
-
Appropriate Legal Notices; however, if the Program has interactive
-
interfaces that do not display Appropriate Legal Notices, your
-
work need not make them do so.
-
-
A compilation of a covered work with other separate and independent
-
works, which are not by their nature extensions of the covered work,
-
and which are not combined with it such as to form a larger program,
-
in or on a volume of a storage or distribution medium, is called an
-
"aggregate" if the compilation and its resulting copyright are not
-
used to limit the access or legal rights of the compilation's users
-
beyond what the individual works permit. Inclusion of a covered work
-
in an aggregate does not cause this License to apply to the other
-
parts of the aggregate.
-
-
6. Conveying Non-Source Forms.
-
-
You may convey a covered work in object code form under the terms
-
of sections 4 and 5, provided that you also convey the
-
machine-readable Corresponding Source under the terms of this License,
-
in one of these ways:
-
-
a) Convey the object code in, or embodied in, a physical product
-
(including a physical distribution medium), accompanied by the
-
Corresponding Source fixed on a durable physical medium
-
customarily used for software interchange.
-
-
b) Convey the object code in, or embodied in, a physical product
-
(including a physical distribution medium), accompanied by a
-
written offer, valid for at least three years and valid for as
-
long as you offer spare parts or customer support for that product
-
model, to give anyone who possesses the object code either (1) a
-
copy of the Corresponding Source for all the software in the
-
product that is covered by this License, on a durable physical
-
medium customarily used for software interchange, for a price no
-
more than your reasonable cost of physically performing this
-
conveying of source, or (2) access to copy the
-
Corresponding Source from a network server at no charge.
-
-
c) Convey individual copies of the object code with a copy of the
-
written offer to provide the Corresponding Source. This
-
alternative is allowed only occasionally and noncommercially, and
-
only if you received the object code with such an offer, in accord
-
with subsection 6b.
-
-
d) Convey the object code by offering access from a designated
-
place (gratis or for a charge), and offer equivalent access to the
-
Corresponding Source in the same way through the same place at no
-
further charge. You need not require recipients to copy the
-
Corresponding Source along with the object code. If the place to
-
copy the object code is a network server, the Corresponding Source
-
may be on a different server (operated by you or a third party)
-
that supports equivalent copying facilities, provided you maintain
-
clear directions next to the object code saying where to find the
-
Corresponding Source. Regardless of what server hosts the
-
Corresponding Source, you remain obligated to ensure that it is
-
available for as long as needed to satisfy these requirements.
-
-
e) Convey the object code using peer-to-peer transmission, provided
-
you inform other peers where the object code and Corresponding
-
Source of the work are being offered to the general public at no
-
charge under subsection 6d.
-
-
A separable portion of the object code, whose source code is excluded
-
from the Corresponding Source as a System Library, need not be
-
included in conveying the object code work.
-
-
A "User Product" is either (1) a "consumer product", which means any
-
tangible personal property which is normally used for personal, family,
-
or household purposes, or (2) anything designed or sold for incorporation
-
into a dwelling. In determining whether a product is a consumer product,
-
doubtful cases shall be resolved in favor of coverage. For a particular
-
product received by a particular user, "normally used" refers to a
-
typical or common use of that class of product, regardless of the status
-
of the particular user or of the way in which the particular user
-
actually uses, or expects or is expected to use, the product. A product
-
is a consumer product regardless of whether the product has substantial
-
commercial, industrial or non-consumer uses, unless such uses represent
-
the only significant mode of use of the product.
-
-
"Installation Information" for a User Product means any methods,
-
procedures, authorization keys, or other information required to install
-
and execute modified versions of a covered work in that User Product from
-
a modified version of its Corresponding Source. The information must
-
suffice to ensure that the continued functioning of the modified object
-
code is in no case prevented or interfered with solely because
-
modification has been made.
-
-
If you convey an object code work under this section in, or with, or
-
specifically for use in, a User Product, and the conveying occurs as
-
part of a transaction in which the right of possession and use of the
-
User Product is transferred to the recipient in perpetuity or for a
-
fixed term (regardless of how the transaction is characterized), the
-
Corresponding Source conveyed under this section must be accompanied
-
by the Installation Information. But this requirement does not apply
-
if neither you nor any third party retains the ability to install
-
modified object code on the User Product (for example, the work has
-
been installed in ROM).
-
-
The requirement to provide Installation Information does not include a
-
requirement to continue to provide support service, warranty, or updates
-
for a work that has been modified or installed by the recipient, or for
-
the User Product in which it has been modified or installed. Access to a
-
network may be denied when the modification itself materially and
-
adversely affects the operation of the network or violates the rules and
-
protocols for communication across the network.
-
-
Corresponding Source conveyed, and Installation Information provided,
-
in accord with this section must be in a format that is publicly
-
documented (and with an implementation available to the public in
-
source code form), and must require no special password or key for
-
unpacking, reading or copying.
-
-
7. Additional Terms.
-
-
"Additional permissions" are terms that supplement the terms of this
-
License by making exceptions from one or more of its conditions.
-
Additional permissions that are applicable to the entire Program shall
-
be treated as though they were included in this License, to the extent
-
that they are valid under applicable law. If additional permissions
-
apply only to part of the Program, that part may be used separately
-
under those permissions, but the entire Program remains governed by
-
this License without regard to the additional permissions.
-
-
When you convey a copy of a covered work, you may at your option
-
remove any additional permissions from that copy, or from any part of
-
it. (Additional permissions may be written to require their own
-
removal in certain cases when you modify the work.) You may place
-
additional permissions on material, added by you to a covered work,
-
for which you have or can give appropriate copyright permission.
-
-
Notwithstanding any other provision of this License, for material you
-
add to a covered work, you may (if authorized by the copyright holders of
-
that material) supplement the terms of this License with terms:
-
-
a) Disclaiming warranty or limiting liability differently from the
-
terms of sections 15 and 16 of this License; or
-
-
b) Requiring preservation of specified reasonable legal notices or
-
author attributions in that material or in the Appropriate Legal
-
Notices displayed by works containing it; or
-
-
c) Prohibiting misrepresentation of the origin of that material, or
-
requiring that modified versions of such material be marked in
-
reasonable ways as different from the original version; or
-
-
d) Limiting the use for publicity purposes of names of licensors or
-
authors of the material; or
-
-
e) Declining to grant rights under trademark law for use of some
-
trade names, trademarks, or service marks; or
-
-
f) Requiring indemnification of licensors and authors of that
-
material by anyone who conveys the material (or modified versions of
-
it) with contractual assumptions of liability to the recipient, for
-
any liability that these contractual assumptions directly impose on
-
those licensors and authors.
-
-
All other non-permissive additional terms are considered "further
-
restrictions" within the meaning of section 10. If the Program as you
-
received it, or any part of it, contains a notice stating that it is
-
governed by this License along with a term that is a further
-
restriction, you may remove that term. If a license document contains
-
a further restriction but permits relicensing or conveying under this
-
License, you may add to a covered work material governed by the terms
-
of that license document, provided that the further restriction does
-
not survive such relicensing or conveying.
+
This version of the GNU Lesser General Public License incorporates
+
the terms and conditions of version 3 of the GNU General Public
+
License, supplemented by the additional permissions listed below.
-
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.
+
0. Additional Definitions.
-
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.
+
As used herein, "this License" refers to version 3 of the GNU Lesser
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
+
General Public License.
-
8. Termination.
+
"The Library" refers to a covered work governed by this License,
+
other than an Application or a Combined Work as defined below.
-
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).
+
An "Application" is any work that makes use of an interface provided
+
by the Library, but which is not otherwise based on the Library.
+
Defining a subclass of a class defined by the Library is deemed a mode
+
of using an interface provided by the Library.
-
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.
+
A "Combined Work" is a work produced by combining or linking an
+
Application with the Library. The particular version of the Library
+
with which the Combined Work was made is also called the "Linked
+
Version".
-
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.
+
The "Minimal Corresponding Source" for a Combined Work means the
+
Corresponding Source for the Combined Work, excluding any source code
+
for portions of the Combined Work that, considered in isolation, are
+
based on the Application, and not on the Linked Version.
-
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.
+
The "Corresponding Application Code" for a Combined Work means the
+
object code and/or source code for the Application, including any data
+
and utility programs needed for reproducing the Combined Work from the
+
Application, but excluding the System Libraries of the Combined Work.
-
9. Acceptance Not Required for Having Copies.
+
1. Exception to Section 3 of the GNU GPL.
-
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.
+
You may convey a covered work under sections 3 and 4 of this License
+
without being bound by section 3 of the GNU GPL.
-
10. Automatic Licensing of Downstream Recipients.
+
2. Conveying Modified Versions.
-
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.
+
If you modify a copy of the Library, and, in your modifications, a
+
facility refers to a function or data to be supplied by an Application
+
that uses the facility (other than as an argument passed when the
+
facility is invoked), then you may convey a copy of the modified
+
version:
-
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.
+
a) under this License, provided that you make a good faith effort to
+
ensure that, in the event an Application does not supply the
+
function or data, the facility still operates, and performs
+
whatever part of its purpose remains meaningful, or
-
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.
+
b) under the GNU GPL, with none of the additional permissions of
+
this License applicable to that copy.
-
11. Patents.
+
3. Object Code Incorporating Material from Library Header Files.
-
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".
+
The object code form of an Application may incorporate material from
+
a header file that is part of the Library. You may convey such object
+
code under terms of your choice, provided that, if the incorporated
+
material is not limited to numerical parameters, data structure
+
layouts and accessors, or small macros, inline functions and templates
+
(ten or fewer lines in length), you do both of the following:
-
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.
+
a) Give prominent notice with each copy of the object code that the
+
Library is used in it and that the Library and its use are
+
covered by 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.
+
b) Accompany the object code with a copy of the GNU GPL and this license
+
document.
-
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.
+
4. Combined Works.
-
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.
+
You may convey a Combined Work under terms of your choice that,
+
taken together, effectively do not restrict modification of the
+
portions of the Library contained in the Combined Work and reverse
+
engineering for debugging such modifications, if you also do each of
+
the following:
-
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) Give prominent notice with each copy of the Combined Work that
+
the Library is used in it and that the Library and its use are
+
covered by this License.
-
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.
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
+
document.
-
Nothing in this License shall be construed as excluding or limiting
-
any implied license or other defenses to infringement that may
-
otherwise be available to you under applicable patent law.
-
-
12. No Surrender of Others' Freedom.
-
-
If conditions are imposed on you (whether by court order, agreement or
-
otherwise) that contradict the conditions of this License, they do not
-
excuse you from the conditions of this License. If you cannot convey a
-
covered work so as to satisfy simultaneously your obligations under this
-
License and any other pertinent obligations, then as a consequence you may
-
not convey it at all. For example, if you agree to terms that obligate you
-
to collect a royalty for further conveying from those to whom you convey
-
the Program, the only way you could satisfy both those terms and this
-
License would be to refrain entirely from conveying the Program.
-
-
13. Remote Network Interaction; Use with the GNU General Public License.
-
-
Notwithstanding any other provision of this License, if you modify the
-
Program, your modified version must prominently offer all users
-
interacting with it remotely through a computer network (if your version
-
supports such interaction) an opportunity to receive the Corresponding
-
Source of your version by providing access to the Corresponding Source
-
from a network server at no charge, through some standard or customary
-
means of facilitating copying of software. This Corresponding Source
-
shall include the Corresponding Source for any work covered by version 3
-
of the GNU General Public License that is incorporated pursuant to the
-
following paragraph.
-
-
Notwithstanding any other provision of this License, you have
-
permission to link or combine any covered work with a work licensed
-
under version 3 of the GNU General Public License into a single
-
combined work, and to convey the resulting work. The terms of this
-
License will continue to apply to the part which is the covered work,
-
but the work with which it is combined will remain governed by version
-
3 of the GNU General Public License.
-
-
14. Revised Versions of this License.
-
-
The Free Software Foundation may publish revised and/or new versions of
-
the GNU Affero General Public License from time to time. Such new versions
-
will be similar in spirit to the present version, but may differ in detail to
-
address new problems or concerns.
+
c) For a Combined Work that displays copyright notices during
+
execution, include the copyright notice for the Library among
+
these notices, as well as a reference directing the user to the
+
copies of the GNU GPL and this license document.
-
Each version is given a distinguishing version number. If the
-
Program specifies that a certain numbered version of the GNU Affero General
-
Public License "or any later version" applies to it, you have the
-
option of following the terms and conditions either of that numbered
-
version or of any later version published by the Free Software
-
Foundation. If the Program does not specify a version number of the
-
GNU Affero General Public License, you may choose any version ever published
-
by the Free Software Foundation.
-
-
If the Program specifies that a proxy can decide which future
-
versions of the GNU Affero General Public License can be used, that proxy's
-
public statement of acceptance of a version permanently authorizes you
-
to choose that version for the Program.
-
-
Later license versions may give you additional or different
-
permissions. However, no additional obligations are imposed on any
-
author or copyright holder as a result of your choosing to follow a
-
later version.
-
-
15. Disclaimer of Warranty.
-
-
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-
16. Limitation of Liability.
-
-
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-
SUCH DAMAGES.
-
-
17. Interpretation of Sections 15 and 16.
-
-
If the disclaimer of warranty and limitation of liability provided
-
above cannot be given local legal effect according to their terms,
-
reviewing courts shall apply local law that most closely approximates
-
an absolute waiver of all civil liability in connection with the
-
Program, unless a warranty or assumption of liability accompanies a
-
copy of the Program in return for a fee.
+
d) Do one of the following:
-
END OF TERMS AND CONDITIONS
+
0) Convey the Minimal Corresponding Source under the terms of this
+
License, and the Corresponding Application Code in a form
+
suitable for, and under terms that permit, the user to
+
recombine or relink the Application with a modified version of
+
the Linked Version to produce a modified Combined Work, in the
+
manner specified by section 6 of the GNU GPL for conveying
+
Corresponding Source.
-
How to Apply These Terms to Your New Programs
+
1) Use a suitable shared library mechanism for linking with the
+
Library. A suitable mechanism is one that (a) uses at run time
+
a copy of the Library already present on the user's computer
+
system, and (b) will operate properly with a modified version
+
of the Library that is interface-compatible with the Linked
+
Version.
-
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.
+
e) Provide Installation Information, but only if you would otherwise
+
be required to provide such information under section 6 of the
+
GNU GPL, and only to the extent that such information is
+
necessary to install and execute a modified version of the
+
Combined Work produced by recombining or relinking the
+
Application with a modified version of the Linked Version. (If
+
you use option 4d0, the Installation Information must accompany
+
the Minimal Corresponding Source and Corresponding Application
+
Code. If you use option 4d1, you must provide the Installation
+
Information in the manner specified by section 6 of the GNU GPL
+
for conveying Corresponding Source.)
-
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.
+
5. Combined Libraries.
-
<one line to give the program's name and a brief idea of what it does.>
-
Copyright (C) <year> <name of author>
+
You may place library facilities that are a work based on the
+
Library side by side in a single library together with other library
+
facilities that are not Applications and are not covered by this
+
License, and convey such a combined library under terms of your
+
choice, if you do both of the following:
-
This program is free software: you can redistribute it and/or modify
-
it under the terms of the GNU Affero General Public License as published by
-
the Free Software Foundation, either version 3 of the License, or
-
(at your option) any later version.
+
a) Accompany the combined library with a copy of the same work based
+
on the Library, uncombined with any other library facilities,
+
conveyed under the terms of this License.
-
This program is distributed in the hope that it will be useful,
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
GNU Affero General Public License for more details.
+
b) Give prominent notice with the combined library that part of it
+
is a work based on the Library, and explaining where to find the
+
accompanying uncombined form of the same work.
-
You should have received a copy of the GNU Affero General Public License
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
+
6. Revised Versions of the GNU Lesser General Public License.
-
Also add information on how to contact you by electronic and paper mail.
+
The Free Software Foundation may publish revised and/or new versions
+
of the GNU Lesser 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.
-
If your software can interact with users remotely through a computer
-
network, you should also make sure that it provides a way for users to
-
get its source. For example, if your program is a web application, its
-
interface could display a "Source" link that leads users to an archive
-
of the code. There are many ways you could offer source, and different
-
solutions will be better for different programs; see section 13 for the
-
specific requirements.
+
Each version is given a distinguishing version number. If the
+
Library as you received it specifies that a certain numbered version
+
of the GNU Lesser General Public License "or any later version"
+
applies to it, you have the option of following the terms and
+
conditions either of that published version or of any later version
+
published by the Free Software Foundation. If the Library as you
+
received it does not specify a version number of the GNU Lesser
+
General Public License, you may choose any version of the GNU Lesser
+
General Public License ever published by the Free Software Foundation.
-
You should also get your employer (if you work as a programmer) or school,
-
if any, to sign a "copyright disclaimer" for the program, if necessary.
-
For more information on this, and how to apply and follow the GNU AGPL, see
-
<https://www.gnu.org/licenses/>.
+
If the Library as you received it specifies that a proxy can decide
+
whether future versions of the GNU Lesser General Public License shall
+
apply, that proxy's public statement of acceptance of any version is
+
permanent authorization for you to choose that version for the
+
Library.
+1 -1
README.md
···
**_This is an experimental passion project._** moonlight was not created out of malicious intent nor intended to seriously compete with other mods. Anything and everything is subject to change.
-
moonlight is licensed under the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) (`AGPL-3.0-or-later`). See [the documentation](https://moonlight-mod.github.io/) for more information.
+
moonlight is licensed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.html) (`LGPL-3.0-or-later`). See [the documentation](https://moonlight-mod.github.io/) for more information.
+17 -5
build.mjs
···
const wpModulesDir = `packages/core-extensions/src/${ext}/webpackModules`;
if (fs.existsSync(wpModulesDir) && side === "index") {
-
const wpModules = fs.readdirSync(wpModulesDir);
-
for (const wpModule of wpModules) {
-
entryPoints.push(
-
`packages/core-extensions/src/${ext}/webpackModules/${wpModule}`
-
);
+
const wpModules = fs.opendirSync(wpModulesDir);
+
for await (const wpModule of wpModules) {
+
if (wpModule.isFile()) {
+
entryPoints.push(
+
`packages/core-extensions/src/${ext}/webpackModules/${wpModule.name}`
+
);
+
} else {
+
for (const fileExt of ["ts", "tsx"]) {
+
const path = `packages/core-extensions/src/${ext}/webpackModules/${wpModule.name}/index.${fileExt}`;
+
if (fs.existsSync(path)) {
+
entryPoints.push({
+
in: path,
+
out: `webpackModules/${wpModule.name}`
+
});
+
}
+
}
+
}
}
}
-1
env.d.ts
···
-
/// <reference types="./packages/types/src/index" />
+2 -1
package.json
···
{
"name": "moonlight",
-
"version": "1.0.7",
+
"version": "1.0.8",
"description": "Yet another Discord mod",
"homepage": "https://moonlight-mod.github.io/",
+
"license": "LGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "git+https://github.com/moonlight-mod/moonlight.git"
+1 -1
packages/core-extensions/src/common/webpackModules/components.ts
···
)[0].exports.default;
const LegacyText = spacepack.findByCode(".selectable", ".colorStandard")[0]
.exports.default;
-
const Flex = spacepack.findByCode(".flex" + "GutterSmall,")[0].exports.default;
+
const Flex = spacepack.findByCode(".flex" + "GutterSmall,")[0].exports.Flex;
const CardClasses = spacepack.findByCode("card", "cardHeader", "inModal")[0]
.exports;
const ControlClasses = spacepack.findByCode(
+2 -1
packages/core-extensions/src/common/webpackModules/flux.ts
···
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
module.exports = spacepack.findByCode(
-
["useStateFromStores", ":function"].join("")
+
["useStateFromStores", ":function"].join(""),
+
"Store:"
)[0].exports;
+37
packages/core-extensions/src/contextMenu/index.tsx
···
+
import { ExtensionWebpackModule, Patch } from "@moonlight-mod/types";
+
+
export const patches: Patch[] = [
+
{
+
find: "Menu API only allows Items and groups of Items as children.",
+
replace: [
+
{
+
match:
+
/(?<=let{navId[^}]+?}=(.),(.)=function .\(.\){.+(?=,.=function))/,
+
replacement: (_, props, items) =>
+
`,__contextMenu=!${props}.__contextMenu_evilMenu&&require("contextMenu_contextMenu")._patchMenu(${props}, ${items})`
+
}
+
]
+
},
+
{
+
find: ".getContextMenu(",
+
replace: [
+
{
+
match: /(?<=let\{[^}]+?\}=.;return ).\({[^}]+?}\)/,
+
replacement: (render) =>
+
`require("contextMenu_contextMenu")._saveProps(this,${render})`
+
}
+
]
+
}
+
];
+
+
export const webpackModules: Record<string, ExtensionWebpackModule> = {
+
contextMenu: {
+
dependencies: [{ ext: "spacepack", id: "spacepack" }, "MenuGroup:"]
+
},
+
evilMenu: {
+
dependencies: [
+
{ ext: "spacepack", id: "spacepack" },
+
"Menu API only allows Items and groups of Items as children."
+
]
+
}
+
};
+9
packages/core-extensions/src/contextMenu/manifest.json
···
+
{
+
"id": "contextMenu",
+
"meta": {
+
"name": "Context Menu",
+
"tagline": "A library for patching and creating context menus",
+
"authors": ["redstonekasi"],
+
"tags": ["library"]
+
}
+
}
+66
packages/core-extensions/src/contextMenu/webpackModules/contextMenu.ts
···
+
import {
+
InternalItem,
+
MenuElement,
+
MenuProps
+
} from "@moonlight-mod/types/coreExtensions/contextMenu";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
import parser from "@moonlight-mod/wp/contextMenu_evilMenu";
+
+
type Patch = {
+
navId: string;
+
item: (
+
props: any
+
) =>
+
| React.ReactComponentElement<MenuElement>
+
| React.ReactComponentElement<MenuElement>[];
+
anchorId: string;
+
before: boolean;
+
};
+
+
export function addItem<T>(
+
navId: string,
+
item: (
+
props: T
+
) =>
+
| React.ReactComponentElement<MenuElement>
+
| React.ReactComponentElement<MenuElement>[],
+
anchorId: string,
+
before = false
+
) {
+
patches.push({ navId, item, anchorId, before });
+
}
+
+
export const patches: Patch[] = [];
+
function _patchMenu(props: MenuProps, items: InternalItem[]) {
+
const matches = patches.filter((p) => p.navId === props.navId);
+
if (!matches.length) return;
+
+
for (const patch of matches) {
+
const idx = items.findIndex((i) => i.key === patch.anchorId);
+
if (idx === -1) continue;
+
items.splice(idx + 1 - +patch.before, 0, ...parser(patch.item(menuProps)));
+
}
+
}
+
+
let menuProps: any;
+
function _saveProps(self: any, el: any) {
+
menuProps = el.props;
+
+
const original = self.props.closeContextMenu;
+
self.props.closeContextMenu = function (...args: any[]) {
+
menuProps = undefined;
+
return original?.apply(this, args);
+
};
+
+
return el;
+
}
+
+
const MenuElements = spacepack.findByCode("return null", "MenuGroup:")[0]
+
.exports;
+
+
module.exports = {
+
...MenuElements,
+
addItem,
+
_patchMenu,
+
_saveProps
+
};
+24
packages/core-extensions/src/contextMenu/webpackModules/evilMenu.ts
···
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
+
let code =
+
spacepack.require.m[
+
spacepack.findByCode(
+
"Menu API only allows Items and groups of Items as children."
+
)[0].id
+
].toString();
+
code = code.replace(/,.=(?=function .\(.\){.+?,.=function)/, ";return ");
+
code = code.replace(/,(?=__contextMenu)/, ";let ");
+
const mod = new Function(
+
"module",
+
"exports",
+
"require",
+
`(${code}).apply(this, arguments)`
+
);
+
const exp: any = {};
+
mod({}, exp, require);
+
module.exports = (el: any) => {
+
return exp.Menu({
+
children: el,
+
__contextMenu_evilMenu: true
+
});
+
};
+4 -5
packages/core-extensions/src/disableSentry/index.ts
···
find: "DSN:function",
replace: {
type: PatchReplaceType.Normal,
-
match: /default:function\(\){return .}/,
-
replacement:
-
'default:function(){return require("disableSentry_stub").proxy()}'
+
match: /(?<=\.default=){.+?}}/,
+
replacement: 'require("disableSentry_stub").proxy()'
}
},
{
···
find: "window.DiscordErrors=",
replace: {
type: PatchReplaceType.Normal,
-
match: /uses_client_mods:\(0,.\.usesClientMods\)\(\)/,
-
replacement: "uses_client_mods:false"
+
match: /\(0,.\.usesClientMods\)\(\)/,
+
replacement: "false"
}
}
];
+9 -2
packages/core-extensions/src/experiments/index.ts
···
export const patches: Patch[] = [
{
-
find: "isStaffEnv:",
+
find: "isStaffPersonal:",
+
replace: {
+
match: /&&null!=this\.personalConnectionId/,
+
replacement: "||!0"
+
}
+
},
+
{
+
find: '"scientist:triggered"', // Scientist? Triggered.
replace: {
-
match: /.\.isStaff\(\)/,
+
match: /(?<=personal_connection_id\|\|)!1/,
replacement: "!0"
}
}
+7 -1
packages/core-extensions/src/experiments/manifest.json
···
"meta": {
"name": "Experiments",
"tagline": "Allows you to configure Discord's internal A/B testing features",
-
"authors": ["NotNite"],
+
"authors": ["NotNite", "Cynosphere"],
"tags": ["dangerZone"]
+
},
+
"settings": {
+
"sections": {
+
"displayName": "Allow access to other staff settings elsewhere",
+
"type": "boolean"
+
}
}
}
+14 -79
packages/core-extensions/src/moonbase/index.tsx
···
-
import { ExtensionWebExports, WebpackRequireType } from "@moonlight-mod/types";
-
import extensionsPage from "./ui/extensions";
-
import configPage from "./ui/config";
+
import { ExtensionWebExports } from "@moonlight-mod/types";
import { CircleXIconSVG, DownloadIconSVG, TrashIconSVG } from "./types";
-
import ui from "./ui";
-
-
export const pageModules: (require: WebpackRequireType) => Record<
-
string,
-
{
-
name: string;
-
element: React.FunctionComponent;
-
}
-
> = (require) => ({
-
extensions: {
-
name: "Extensions",
-
element: extensionsPage(require)
-
},
-
config: {
-
name: "Config",
-
element: configPage(require)
-
}
-
});
export const webpackModules: ExtensionWebExports["webpackModules"] = {
stores: {
···
]
},
-
moonbase: {
+
ui: {
dependencies: [
{ ext: "spacepack", id: "spacepack" },
-
{ ext: "settings", id: "settings" },
{ ext: "common", id: "react" },
{ ext: "common", id: "components" },
{ ext: "moonbase", id: "stores" },
···
"removeButtonContainer:",
'"Missing channel in Channel.openChannelContextMenu"',
".default.HEADER_BAR"
+
]
+
},
+
+
moonbase: {
+
dependencies: [
+
{ ext: "spacepack", id: "spacepack" },
+
{ ext: "settings", id: "settings" },
+
{ ext: "common", id: "react" },
+
{ ext: "moonbase", id: "ui" }
],
-
entrypoint: true,
-
run: (module, exports, require) => {
-
const settings = require("settings_settings").Settings;
-
const React = require("common_react");
-
const spacepack = require("spacepack_spacepack").spacepack;
-
const { MoonbaseSettingsStore } =
-
require("moonbase_stores") as typeof import("./webpackModules/stores");
-
-
const addSection = (name: string, element: React.FunctionComponent) => {
-
settings.addSection(name, name, element, null, -2, {
-
stores: [MoonbaseSettingsStore],
-
element: () => {
-
// Require it here because lazy loading SUX
-
const SettingsNotice =
-
spacepack.findByCode("onSaveButtonColor")[0].exports.default;
-
return (
-
<SettingsNotice
-
submitting={MoonbaseSettingsStore.submitting}
-
onReset={() => {
-
MoonbaseSettingsStore.reset();
-
}}
-
onSave={() => {
-
MoonbaseSettingsStore.writeConfig();
-
}}
-
/>
-
);
-
}
-
});
-
};
-
-
if (moonlight.getConfigOption<boolean>("moonbase", "sections")) {
-
const pages = pageModules(require);
-
-
const { Text } = require("common_components");
-
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
-
-
settings.addHeader("Moonbase", -2);
-
for (const page of Object.values(pages)) {
-
addSection(page.name, () => (
-
<>
-
<Text
-
className={Margins.marginBottom20}
-
variant="heading-lg/semibold"
-
tag="h2"
-
>
-
Extensions
-
</Text>
-
<page.element />
-
</>
-
));
-
}
-
} else {
-
addSection("Moonbase", ui(require));
-
}
-
}
+
entrypoint: true
}
};
export const styles = [
-
".moonbase-settings > :first-child { margin-top: 0px; }"
+
".moonbase-settings > :first-child { margin-top: 0px; }",
+
"textarea.moonbase-resizeable { resize: vertical }"
];
+1
packages/core-extensions/src/moonbase/types.ts
···
export type MoonbaseExtension = {
id: string;
+
uniqueId: number;
manifest: ExtensionManifest | RepositoryManifest;
source: DetectedExtension["source"];
state: ExtensionState;
-157
packages/core-extensions/src/moonbase/ui/config/index.tsx
···
-
import { LogLevel, WebpackRequireType } from "@moonlight-mod/types";
-
import { CircleXIconSVG } from "../../types";
-
-
const logLevels = Object.values(LogLevel).filter(
-
(v) => typeof v === "string"
-
) as string[];
-
-
export default (require: WebpackRequireType) => {
-
const React = require("common_react");
-
const spacepack = require("spacepack_spacepack").spacepack;
-
const CommonComponents = require("common_components");
-
const {
-
FormDivider,
-
FormItem,
-
FormText,
-
FormSwitch,
-
TextInput,
-
Flex,
-
Button,
-
SingleSelect
-
} = CommonComponents;
-
-
const { MoonbaseSettingsStore } =
-
require("moonbase_stores") as typeof import("../../webpackModules/stores");
-
-
const FormClasses = spacepack.findByCode("dividerDefault:")[0].exports;
-
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
-
-
const RemoveButtonClasses = spacepack.findByCode("removeButtonContainer")[0]
-
.exports;
-
const CircleXIcon = spacepack.findByCode(CircleXIconSVG)[0].exports.default;
-
function RemoveEntryButton({ onClick }: { onClick: () => void }) {
-
const { Tooltip, Clickable } = CommonComponents;
-
return (
-
<div className={RemoveButtonClasses.removeButtonContainer}>
-
<Tooltip text="Remove entry" position="top">
-
{(props: any) => (
-
<Clickable
-
{...props}
-
className={RemoveButtonClasses.removeButton}
-
onClick={onClick}
-
>
-
<CircleXIcon width={24} height={24} />
-
</Clickable>
-
)}
-
</Tooltip>
-
</div>
-
);
-
}
-
-
function ArrayFormItem({
-
config
-
}: {
-
config: "repositories" | "devSearchPaths";
-
}) {
-
const items = MoonbaseSettingsStore.getConfigOption(config) ?? [];
-
return (
-
<Flex
-
style={{
-
gap: "20px"
-
}}
-
direction={Flex.Direction.VERTICAL}
-
>
-
{items.map((val, i) => (
-
<div
-
key={i}
-
style={{
-
display: "grid",
-
height: "32px",
-
gap: "8px",
-
gridTemplateColumns: "1fr 32px",
-
alignItems: "center"
-
}}
-
>
-
<TextInput
-
size={TextInput.Sizes.DEFAULT}
-
value={val}
-
onChange={(newVal: string) => {
-
items[i] = newVal;
-
MoonbaseSettingsStore.setConfigOption(config, items);
-
}}
-
/>
-
<RemoveEntryButton
-
onClick={() => {
-
items.splice(i, 1);
-
MoonbaseSettingsStore.setConfigOption(config, items);
-
}}
-
/>
-
</div>
-
))}
-
-
<Button
-
look={Button.Looks.FILLED}
-
color={Button.Colors.GREEN}
-
size={Button.Sizes.SMALL}
-
style={{
-
marginTop: "10px"
-
}}
-
onClick={() => {
-
items.push("");
-
MoonbaseSettingsStore.setConfigOption(config, items);
-
}}
-
>
-
Add new entry
-
</Button>
-
</Flex>
-
);
-
}
-
-
return function ConfigPage() {
-
return (
-
<>
-
<FormItem title="Repositories">
-
<FormText className={Margins.marginBottom4}>
-
A list of remote repositories to display extensions from
-
</FormText>
-
<ArrayFormItem config="repositories" />
-
</FormItem>
-
<FormDivider className={FormClasses.dividerDefault} />
-
<FormItem
-
title="Extension search paths"
-
className={Margins.marginTop20}
-
>
-
<FormText className={Margins.marginBottom4}>
-
A list of local directories to search for built extensions
-
</FormText>
-
<ArrayFormItem config="devSearchPaths" />
-
</FormItem>
-
<FormDivider className={FormClasses.dividerDefault} />
-
<FormSwitch
-
className={Margins.marginTop20}
-
value={MoonbaseSettingsStore.getConfigOption("patchAll")}
-
onChange={(value: boolean) => {
-
MoonbaseSettingsStore.setConfigOption("patchAll", value);
-
}}
-
note="Wraps every webpack module in a function, separating them in DevTools"
-
>
-
Patch all
-
</FormSwitch>
-
<FormItem title="Log level">
-
<SingleSelect
-
autofocus={false}
-
clearable={false}
-
value={MoonbaseSettingsStore.getConfigOption("loggerLevel")}
-
options={logLevels.map((o) => ({
-
value: o.toLowerCase(),
-
label: o[0] + o.slice(1).toLowerCase()
-
}))}
-
onChange={(v) =>
-
MoonbaseSettingsStore.setConfigOption("loggerLevel", v)
-
}
-
/>
-
</FormItem>
-
</>
-
);
-
};
-
};
-223
packages/core-extensions/src/moonbase/ui/extensions/card.tsx
···
-
import WebpackRequire from "@moonlight-mod/types/discord/require";
-
import {
-
DangerIconSVG,
-
DownloadIconSVG,
-
ExtensionState,
-
TrashIconSVG
-
} from "../../types";
-
import { ExtensionLoadSource } from "@moonlight-mod/types";
-
import info from "./info";
-
import settings from "./settings";
-
-
export enum ExtensionPage {
-
Info,
-
Description,
-
Settings
-
}
-
-
export default (require: typeof WebpackRequire) => {
-
const React = require("common_react");
-
const spacepack = require("spacepack_spacepack").spacepack;
-
const CommonComponents = require("common_components");
-
const Flux = require("common_flux");
-
-
const { ExtensionInfo } = info(require);
-
const Settings = settings(require);
-
const { MoonbaseSettingsStore } =
-
require("moonbase_stores") as typeof import("../../webpackModules/stores");
-
-
const UserProfileClasses = spacepack.findByCode(
-
"tabBarContainer",
-
"topSection"
-
)[0].exports;
-
-
const DownloadIcon =
-
spacepack.findByCode(DownloadIconSVG)[0].exports.DownloadIcon;
-
const TrashIcon = spacepack.findByCode(TrashIconSVG)[0].exports.default;
-
const DangerIcon =
-
spacepack.findByCode(DangerIconSVG)[0].exports.CircleExclamationPointIcon;
-
-
const PanelButton =
-
spacepack.findByCode("Masks.PANEL_BUTTON")[0].exports.default;
-
-
return function ExtensionCard({ id }: { id: string }) {
-
const [tab, setTab] = React.useState(ExtensionPage.Info);
-
const [restartNeeded, setRestartNeeded] = React.useState(false);
-
-
const { ext, enabled, busy, update } = Flux.useStateFromStores(
-
[MoonbaseSettingsStore],
-
() => {
-
return {
-
ext: MoonbaseSettingsStore.getExtension(id),
-
enabled: MoonbaseSettingsStore.getExtensionEnabled(id),
-
busy: MoonbaseSettingsStore.busy,
-
update: MoonbaseSettingsStore.getExtensionUpdate(id)
-
};
-
}
-
);
-
-
// Why it work like that :sob:
-
if (ext == null) return <></>;
-
-
const {
-
Card,
-
CardClasses,
-
Flex,
-
Text,
-
MarkdownParser,
-
Switch,
-
TabBar,
-
Button
-
} = CommonComponents;
-
-
const tagline = ext.manifest?.meta?.tagline;
-
const settings = ext.manifest?.settings;
-
const description = ext.manifest?.meta?.description;
-
-
return (
-
<Card editable={true} className={CardClasses.card}>
-
<div className={CardClasses.cardHeader}>
-
<Flex direction={Flex.Direction.VERTICAL}>
-
<Flex direction={Flex.Direction.HORIZONTAL}>
-
<Text variant="text-md/semibold">
-
{ext.manifest?.meta?.name ?? ext.id}
-
</Text>
-
</Flex>
-
-
{tagline != null && (
-
<Text variant="text-sm/normal">
-
{MarkdownParser.parse(tagline)}
-
</Text>
-
)}
-
</Flex>
-
-
<Flex
-
direction={Flex.Direction.HORIZONTAL}
-
align={Flex.Align.END}
-
justify={Flex.Justify.END}
-
>
-
{ext.state === ExtensionState.NotDownloaded ? (
-
<Button
-
color={Button.Colors.BRAND}
-
submitting={busy}
-
onClick={() => {
-
MoonbaseSettingsStore.installExtension(id);
-
}}
-
>
-
Install
-
</Button>
-
) : (
-
<div
-
// too lazy to learn how <Flex /> works lmao
-
style={{
-
display: "flex",
-
alignItems: "center",
-
gap: "1rem"
-
}}
-
>
-
{ext.source.type === ExtensionLoadSource.Normal && (
-
<PanelButton
-
icon={TrashIcon}
-
tooltipText="Delete"
-
onClick={() => {
-
MoonbaseSettingsStore.deleteExtension(id);
-
}}
-
/>
-
)}
-
-
{update !== null && (
-
<PanelButton
-
icon={DownloadIcon}
-
tooltipText="Update"
-
onClick={() => {
-
MoonbaseSettingsStore.installExtension(id);
-
}}
-
/>
-
)}
-
-
{restartNeeded && (
-
<PanelButton
-
icon={() => (
-
<DangerIcon
-
color={CommonComponents.tokens.colors.STATUS_DANGER}
-
/>
-
)}
-
onClick={() => window.location.reload()}
-
tooltipText="You will need to reload/restart your client for this extension to work properly."
-
/>
-
)}
-
-
<Switch
-
checked={enabled}
-
onChange={() => {
-
setRestartNeeded(true);
-
MoonbaseSettingsStore.setExtensionEnabled(id, !enabled);
-
}}
-
/>
-
</div>
-
)}
-
</Flex>
-
</div>
-
-
<div className={UserProfileClasses.body}>
-
{(description != null || settings != null) && (
-
<div
-
className={UserProfileClasses.tabBarContainer}
-
style={{
-
padding: "0 10px"
-
}}
-
>
-
<TabBar
-
selectedItem={tab}
-
type="top"
-
onItemSelect={setTab}
-
className={UserProfileClasses.tabBar}
-
>
-
<TabBar.Item
-
className={UserProfileClasses.tabBarItem}
-
id={ExtensionPage.Info}
-
>
-
Info
-
</TabBar.Item>
-
-
{description != null && (
-
<TabBar.Item
-
className={UserProfileClasses.tabBarItem}
-
id={ExtensionPage.Description}
-
>
-
Description
-
</TabBar.Item>
-
)}
-
-
{settings != null && (
-
<TabBar.Item
-
className={UserProfileClasses.tabBarItem}
-
id={ExtensionPage.Settings}
-
>
-
Settings
-
</TabBar.Item>
-
)}
-
</TabBar>
-
</div>
-
)}
-
-
<Flex
-
justify={Flex.Justify.START}
-
wrap={Flex.Wrap.WRAP}
-
style={{
-
padding: "16px 16px"
-
}}
-
>
-
{tab === ExtensionPage.Info && <ExtensionInfo ext={ext} />}
-
{tab === ExtensionPage.Description && (
-
<Text variant="text-md/normal">
-
{MarkdownParser.parse(description ?? "*No description*")}
-
</Text>
-
)}
-
{tab === ExtensionPage.Settings && <Settings ext={ext} />}
-
</Flex>
-
</div>
-
</Card>
-
);
-
};
-
};
-373
packages/core-extensions/src/moonbase/ui/extensions/filterBar.tsx
···
-
import { WebpackRequireType } from "@moonlight-mod/types";
-
import { tagNames } from "./info";
-
import {
-
ArrowsUpDownIconSVG,
-
ChevronSmallDownIconSVG,
-
ChevronSmallUpIconSVG
-
} from "../../types";
-
-
export enum Filter {
-
Core = 1 << 0,
-
Normal = 1 << 1,
-
Developer = 1 << 2,
-
Enabled = 1 << 3,
-
Disabled = 1 << 4,
-
Installed = 1 << 5,
-
Repository = 1 << 6
-
}
-
export const defaultFilter = ~(~0 << 7);
-
-
export default async (require: WebpackRequireType) => {
-
const spacepack = require("spacepack_spacepack").spacepack;
-
const React = require("common_react");
-
const Flux = require("common_flux");
-
const { WindowStore } = require("common_stores");
-
-
const {
-
Button,
-
Text,
-
Heading,
-
Popout,
-
Dialog
-
} = require("common_components");
-
-
const channelModule =
-
require.m[
-
spacepack.findByCode(
-
'"Missing channel in Channel.openChannelContextMenu"'
-
)[0].id
-
].toString();
-
const moduleId = channelModule.match(/webpackId:"(.+?)"/)![1];
-
await require.el(moduleId);
-
-
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
-
const SortMenuClasses = spacepack.findByCode("container:", "clearText:")[0]
-
.exports;
-
const FilterDialogClasses = spacepack.findByCode(
-
"countContainer:",
-
"tagContainer:"
-
)[0].exports;
-
const FilterBarClasses = spacepack.findByCode("tagsButtonWithCount:")[0]
-
.exports;
-
-
const TagItem = spacepack.findByCode("IncreasedActivityForumTagPill:")[0]
-
.exports.default;
-
-
const ChevronSmallDownIcon = spacepack.findByCode(ChevronSmallDownIconSVG)[0]
-
.exports.default;
-
const ChevronSmallUpIcon = spacepack.findByCode(ChevronSmallUpIconSVG)[0]
-
.exports.default;
-
const ArrowsUpDownIcon =
-
spacepack.findByCode(ArrowsUpDownIconSVG)[0].exports.default;
-
-
function toggleTag(
-
selectedTags: Set<string>,
-
setSelectedTags: (tags: Set<string>) => void,
-
tag: string
-
) {
-
const newState = new Set(selectedTags);
-
if (newState.has(tag)) newState.delete(tag);
-
else newState.add(tag);
-
setSelectedTags(newState);
-
}
-
-
function FilterButtonPopout({
-
filter,
-
setFilter,
-
closePopout
-
}: {
-
filter: Filter;
-
setFilter: (filter: Filter) => void;
-
closePopout: () => void;
-
}) {
-
const {
-
Menu,
-
MenuItem,
-
MenuGroup,
-
MenuCheckboxItem
-
} = require("common_components");
-
-
const toggleFilter = (set: Filter) =>
-
setFilter(filter & set ? filter & ~set : filter | set);
-
-
return (
-
<div className={SortMenuClasses.container}>
-
<Menu navId="sort-filter" hideScrollbar={true} onClose={closePopout}>
-
<MenuGroup label="Type">
-
<MenuCheckboxItem
-
id="t-core"
-
label="Core"
-
checked={filter & Filter.Core}
-
action={() => toggleFilter(Filter.Core)}
-
/>
-
<MenuCheckboxItem
-
id="t-normal"
-
label="Normal"
-
checked={filter & Filter.Normal}
-
action={() => toggleFilter(Filter.Normal)}
-
/>
-
<MenuCheckboxItem
-
id="t-developer"
-
label="Developer"
-
checked={filter & Filter.Developer}
-
action={() => toggleFilter(Filter.Developer)}
-
/>
-
</MenuGroup>
-
<MenuGroup label="State">
-
<MenuCheckboxItem
-
id="s-enabled"
-
label="Enabled"
-
checked={filter & Filter.Enabled}
-
action={() => toggleFilter(Filter.Enabled)}
-
/>
-
<MenuCheckboxItem
-
id="s-disabled"
-
label="Disabled"
-
checked={filter & Filter.Disabled}
-
action={() => toggleFilter(Filter.Disabled)}
-
/>
-
</MenuGroup>
-
<MenuGroup label="Location">
-
<MenuCheckboxItem
-
id="l-installed"
-
label="Installed"
-
checked={filter & Filter.Installed}
-
action={() => toggleFilter(Filter.Installed)}
-
/>
-
<MenuCheckboxItem
-
id="l-repository"
-
label="Repository"
-
checked={filter & Filter.Repository}
-
action={() => toggleFilter(Filter.Repository)}
-
/>
-
</MenuGroup>
-
<MenuGroup>
-
<MenuItem
-
id="reset-all"
-
className={SortMenuClasses.clearText}
-
label={
-
<Text variant="text-sm/medium" color="none">
-
Reset to default
-
</Text>
-
}
-
action={() => {
-
setFilter(defaultFilter);
-
closePopout();
-
}}
-
/>
-
</MenuGroup>
-
</Menu>
-
</div>
-
);
-
}
-
-
function TagButtonPopout({
-
selectedTags,
-
setSelectedTags,
-
setPopoutRef,
-
closePopout
-
}: any) {
-
return (
-
<Dialog ref={setPopoutRef} className={FilterDialogClasses.container}>
-
<div className={FilterDialogClasses.header}>
-
<div className={FilterDialogClasses.headerLeft}>
-
<Heading
-
color="interactive-normal"
-
variant="text-xs/bold"
-
className={FilterDialogClasses.headerText}
-
>
-
Select tags
-
</Heading>
-
<div className={FilterDialogClasses.countContainer}>
-
<Text
-
className={FilterDialogClasses.countText}
-
color="none"
-
variant="text-xs/medium"
-
>
-
{selectedTags.size}
-
</Text>
-
</div>
-
</div>
-
</div>
-
<div className={FilterDialogClasses.tagContainer}>
-
{Object.keys(tagNames).map((tag) => (
-
<TagItem
-
key={tag}
-
className={FilterDialogClasses.tag}
-
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
-
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
-
selected={selectedTags.has(tag)}
-
/>
-
))}
-
</div>
-
<div className={FilterDialogClasses.separator} />
-
<Button
-
look={Button.Looks.LINK}
-
size={Button.Sizes.MIN}
-
color={Button.Colors.CUSTOM}
-
className={FilterDialogClasses.clear}
-
onClick={() => {
-
setSelectedTags(new Set());
-
closePopout();
-
}}
-
>
-
<Text variant="text-sm/medium" color="text-link">
-
Clear all
-
</Text>
-
</Button>
-
</Dialog>
-
);
-
}
-
-
return function FilterBar({
-
filter,
-
setFilter,
-
selectedTags,
-
setSelectedTags
-
}: {
-
filter: Filter;
-
setFilter: (filter: Filter) => void;
-
selectedTags: Set<string>;
-
setSelectedTags: (tags: Set<string>) => void;
-
}) {
-
const windowSize = Flux.useStateFromStores([WindowStore], () =>
-
WindowStore.windowSize()
-
);
-
-
const tagsContainer = React.useRef<HTMLDivElement>(null);
-
const tagListInner = React.useRef<HTMLDivElement>(null);
-
const [tagsButtonOffset, setTagsButtonOffset] = React.useState(0);
-
React.useLayoutEffect(() => {
-
if (tagsContainer.current === null || tagListInner.current === null)
-
return;
-
const { left: containerX, top: containerY } =
-
tagsContainer.current.getBoundingClientRect();
-
let offset = 0;
-
for (const child of tagListInner.current.children) {
-
const {
-
right: childX,
-
top: childY,
-
height
-
} = child.getBoundingClientRect();
-
if (childY - containerY > height) break;
-
const newOffset = childX - containerX;
-
if (newOffset > offset) {
-
offset = newOffset;
-
}
-
}
-
setTagsButtonOffset(offset);
-
}, [windowSize]);
-
-
return (
-
<div
-
ref={tagsContainer}
-
style={{
-
paddingTop: "12px"
-
}}
-
className={`${FilterBarClasses.tagsContainer} ${Margins.marginBottom8}`}
-
>
-
<Popout
-
renderPopout={({ closePopout }: any) => (
-
<FilterButtonPopout
-
filter={filter}
-
setFilter={setFilter}
-
closePopout={closePopout}
-
/>
-
)}
-
position="bottom"
-
align="left"
-
>
-
{(props: any, { isShown }: { isShown: boolean }) => (
-
<Button
-
{...props}
-
size={Button.Sizes.MIN}
-
color={Button.Colors.CUSTOM}
-
className={FilterBarClasses.sortDropdown}
-
innerClassName={FilterBarClasses.sortDropdownInner}
-
>
-
<ArrowsUpDownIcon />
-
<Text
-
className={FilterBarClasses.sortDropdownText}
-
variant="text-sm/medium"
-
color="interactive-normal"
-
>
-
Sort & filter
-
</Text>
-
{isShown ? (
-
<ChevronSmallUpIcon size={20} />
-
) : (
-
<ChevronSmallDownIcon size={20} />
-
)}
-
</Button>
-
)}
-
</Popout>
-
<div className={FilterBarClasses.divider} />
-
<div className={FilterBarClasses.tagList}>
-
<div ref={tagListInner} className={FilterBarClasses.tagListInner}>
-
{Object.keys(tagNames).map((tag) => (
-
<TagItem
-
key={tag}
-
className={FilterBarClasses.tag}
-
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
-
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
-
selected={selectedTags.has(tag)}
-
/>
-
))}
-
</div>
-
</div>
-
<Popout
-
renderPopout={({ setPopoutRef, closePopout }: any) => (
-
<TagButtonPopout
-
selectedTags={selectedTags}
-
setSelectedTags={setSelectedTags}
-
setPopoutRef={setPopoutRef}
-
closePopout={closePopout}
-
/>
-
)}
-
position="bottom"
-
align="right"
-
>
-
{(props: any, { isShown }: { isShown: boolean }) => (
-
<Button
-
{...props}
-
size={Button.Sizes.MIN}
-
color={Button.Colors.CUSTOM}
-
style={{
-
left: tagsButtonOffset
-
}}
-
// TODO: Use Discord's class name utility
-
className={`${FilterBarClasses.tagsButton} ${
-
selectedTags.size > 0
-
? FilterBarClasses.tagsButtonWithCount
-
: ""
-
}`}
-
innerClassName={FilterBarClasses.tagsButtonInner}
-
>
-
{selectedTags.size > 0 ? (
-
<div
-
style={{ boxSizing: "content-box" }}
-
className={FilterBarClasses.countContainer}
-
>
-
<Text
-
className={FilterBarClasses.countText}
-
color="none"
-
variant="text-xs/medium"
-
>
-
{selectedTags.size}
-
</Text>
-
</div>
-
) : (
-
<>All</>
-
)}
-
{isShown ? (
-
<ChevronSmallUpIcon size={20} />
-
) : (
-
<ChevronSmallDownIcon size={20} />
-
)}
-
</Button>
-
)}
-
</Popout>
-
</div>
-
);
-
};
-
};
-118
packages/core-extensions/src/moonbase/ui/extensions/index.tsx
···
-
import {
-
ExtensionLoadSource,
-
ExtensionTag,
-
WebpackRequireType
-
} from "@moonlight-mod/types";
-
import { ExtensionState } from "../../types";
-
import filterBar, { Filter, defaultFilter } from "./filterBar";
-
import card from "./card";
-
-
export default (require: WebpackRequireType) => {
-
const React = require("common_react");
-
const spacepack = require("spacepack_spacepack").spacepack;
-
const Flux = require("common_flux");
-
-
const { MoonbaseSettingsStore } =
-
require("moonbase_stores") as typeof import("../../webpackModules/stores");
-
-
const ExtensionCard = card(require);
-
const FilterBar = React.lazy(() =>
-
filterBar(require).then((c) => ({ default: c }))
-
);
-
-
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
-
const SearchBar = spacepack.findByCode("Messages.SEARCH", "hideSearchIcon")[0]
-
.exports.default;
-
-
return function ExtensionsPage() {
-
const { extensions, savedFilter } = Flux.useStateFromStoresObject(
-
[MoonbaseSettingsStore],
-
() => {
-
return {
-
extensions: MoonbaseSettingsStore.extensions,
-
savedFilter: MoonbaseSettingsStore.getExtensionConfig(
-
"moonbase",
-
"filter"
-
)
-
};
-
}
-
);
-
-
const [query, setQuery] = React.useState("");
-
-
let filter: Filter, setFilter: (filter: Filter) => void;
-
if (moonlight.getConfigOption<boolean>("moonbase", "saveFilter")) {
-
filter = savedFilter ?? defaultFilter;
-
setFilter = (filter) =>
-
MoonbaseSettingsStore.setExtensionConfig("moonbase", "filter", filter);
-
} else {
-
const state = React.useState(defaultFilter);
-
filter = state[0];
-
setFilter = state[1];
-
}
-
const [selectedTags, setSelectedTags] = React.useState(new Set<string>());
-
const sorted = Object.values(extensions).sort((a, b) => {
-
const aName = a.manifest.meta?.name ?? a.id;
-
const bName = b.manifest.meta?.name ?? b.id;
-
return aName.localeCompare(bName);
-
});
-
-
const filtered = sorted.filter(
-
(ext) =>
-
(ext.manifest.meta?.name?.toLowerCase().includes(query) ||
-
ext.manifest.meta?.tagline?.toLowerCase().includes(query) ||
-
ext.manifest.meta?.description?.toLowerCase().includes(query)) &&
-
[...selectedTags.values()].every(
-
(tag) => ext.manifest.meta?.tags?.includes(tag as ExtensionTag)
-
) &&
-
// This seems very bad, sorry
-
!(
-
(!(filter & Filter.Core) &&
-
ext.source.type === ExtensionLoadSource.Core) ||
-
(!(filter & Filter.Normal) &&
-
ext.source.type === ExtensionLoadSource.Normal) ||
-
(!(filter & Filter.Developer) &&
-
ext.source.type === ExtensionLoadSource.Developer) ||
-
(!(filter & Filter.Enabled) &&
-
MoonbaseSettingsStore.getExtensionEnabled(ext.id)) ||
-
(!(filter & Filter.Disabled) &&
-
!MoonbaseSettingsStore.getExtensionEnabled(ext.id)) ||
-
(!(filter & Filter.Installed) &&
-
ext.state !== ExtensionState.NotDownloaded) ||
-
(!(filter & Filter.Repository) &&
-
ext.state === ExtensionState.NotDownloaded)
-
)
-
);
-
-
return (
-
<>
-
<SearchBar
-
size={SearchBar.Sizes.MEDIUM}
-
query={query}
-
onChange={(v: string) => setQuery(v.toLowerCase())}
-
onClear={() => setQuery("")}
-
autoFocus={true}
-
autoComplete="off"
-
inputProps={{
-
autoCapitalize: "none",
-
autoCorrect: "off",
-
spellCheck: "false"
-
}}
-
/>
-
<React.Suspense
-
fallback={<div className={Margins.marginBottom20}></div>}
-
>
-
<FilterBar
-
filter={filter}
-
setFilter={setFilter}
-
selectedTags={selectedTags}
-
setSelectedTags={setSelectedTags}
-
/>
-
</React.Suspense>
-
{filtered.map((ext) => (
-
<ExtensionCard id={ext.id} key={ext.id} />
-
))}
-
</>
-
);
-
};
-
};
-209
packages/core-extensions/src/moonbase/ui/extensions/info.tsx
···
-
import WebpackRequire from "@moonlight-mod/types/discord/require";
-
import { ExtensionTag } from "@moonlight-mod/types";
-
import { MoonbaseExtension } from "../../types";
-
-
type Dependency = {
-
id: string;
-
type: DependencyType;
-
};
-
-
enum DependencyType {
-
Dependency = "dependency",
-
Optional = "optional",
-
Incompatible = "incompatible"
-
}
-
-
export const tagNames: Record<ExtensionTag, string> = {
-
[ExtensionTag.Accessibility]: "Accessibility",
-
[ExtensionTag.Appearance]: "Appearance",
-
[ExtensionTag.Chat]: "Chat",
-
[ExtensionTag.Commands]: "Commands",
-
[ExtensionTag.ContextMenu]: "Context Menu",
-
[ExtensionTag.DangerZone]: "Danger Zone",
-
[ExtensionTag.Development]: "Development",
-
[ExtensionTag.Fixes]: "Fixes",
-
[ExtensionTag.Fun]: "Fun",
-
[ExtensionTag.Markdown]: "Markdown",
-
[ExtensionTag.Voice]: "Voice",
-
[ExtensionTag.Privacy]: "Privacy",
-
[ExtensionTag.Profiles]: "Profiles",
-
[ExtensionTag.QualityOfLife]: "Quality of Life",
-
[ExtensionTag.Library]: "Library"
-
};
-
-
export default (require: typeof WebpackRequire) => {
-
const React = require("common_react");
-
const spacepack = require("spacepack_spacepack").spacepack;
-
-
const CommonComponents = require("common_components");
-
const UserInfoClasses = spacepack.findByCode(
-
"infoScroller",
-
"userInfoSection",
-
"userInfoSectionHeader"
-
)[0].exports;
-
-
const { MoonbaseSettingsStore } =
-
require("moonbase_stores") as typeof import("../../webpackModules/stores");
-
-
function InfoSection({
-
title,
-
children
-
}: {
-
title: string;
-
children: React.ReactNode;
-
}) {
-
return (
-
<div
-
style={{
-
marginRight: "1em"
-
}}
-
>
-
<CommonComponents.Text
-
variant="eyebrow"
-
className={UserInfoClasses.userInfoSectionHeader}
-
>
-
{title}
-
</CommonComponents.Text>
-
-
<CommonComponents.Text variant="text-sm/normal">
-
{children}
-
</CommonComponents.Text>
-
</div>
-
);
-
}
-
-
function Badge({
-
color,
-
children
-
}: {
-
color: string;
-
children: React.ReactNode;
-
}) {
-
return (
-
<span
-
style={{
-
borderRadius: ".1875rem",
-
padding: "0 0.275rem",
-
marginRight: "0.4em",
-
backgroundColor: color,
-
color: "#fff"
-
}}
-
>
-
{children}
-
</span>
-
);
-
}
-
-
function ExtensionInfo({ ext }: { ext: MoonbaseExtension }) {
-
const authors = ext.manifest?.meta?.authors;
-
const tags = ext.manifest?.meta?.tags;
-
const version = ext.manifest?.version;
-
-
const dependencies: Dependency[] = [];
-
if (ext.manifest.dependencies != null) {
-
dependencies.push(
-
...ext.manifest.dependencies.map((dep) => ({
-
id: dep,
-
type: DependencyType.Dependency
-
}))
-
);
-
}
-
-
if (ext.manifest.suggested != null) {
-
dependencies.push(
-
...ext.manifest.suggested.map((dep) => ({
-
id: dep,
-
type: DependencyType.Optional
-
}))
-
);
-
}
-
-
if (ext.manifest.incompatible != null) {
-
dependencies.push(
-
...ext.manifest.incompatible.map((dep) => ({
-
id: dep,
-
type: DependencyType.Incompatible
-
}))
-
);
-
}
-
-
return (
-
<>
-
{authors != null && (
-
<InfoSection title="Authors">
-
{authors.map((author, i) => {
-
const comma = i !== authors.length - 1 ? ", " : "";
-
if (typeof author === "string") {
-
return (
-
<span key={i}>
-
{author}
-
{comma}
-
</span>
-
);
-
} else {
-
// TODO: resolve IDs
-
return (
-
<span key={i}>
-
{author.name}
-
{comma}
-
</span>
-
);
-
}
-
})}
-
</InfoSection>
-
)}
-
-
{tags != null && (
-
<InfoSection title="Tags">
-
{tags.map((tag, i) => {
-
const name = tagNames[tag];
-
-
return (
-
<Badge
-
key={i}
-
color={
-
tag === ExtensionTag.DangerZone
-
? "var(--red-400)"
-
: "var(--brand-500)"
-
}
-
>
-
{name}
-
</Badge>
-
);
-
})}
-
</InfoSection>
-
)}
-
-
{dependencies.length > 0 && (
-
<InfoSection title="Dependencies">
-
{dependencies.map((dep) => {
-
const colors = {
-
[DependencyType.Dependency]: "var(--brand-500)",
-
[DependencyType.Optional]: "var(--orange-400)",
-
[DependencyType.Incompatible]: "var(--red-400)"
-
};
-
const color = colors[dep.type];
-
const name = MoonbaseSettingsStore.getExtensionName(dep.id);
-
return (
-
<Badge color={color} key={dep.id}>
-
{name}
-
</Badge>
-
);
-
})}
-
</InfoSection>
-
)}
-
-
{version != null && (
-
<InfoSection title="Version">
-
<span>{version}</span>
-
</InfoSection>
-
)}
-
</>
-
);
-
}
-
-
return {
-
InfoSection,
-
ExtensionInfo
-
};
-
};
-396
packages/core-extensions/src/moonbase/ui/extensions/settings.tsx
···
-
import {
-
ExtensionSettingType,
-
ExtensionSettingsManifest,
-
MultiSelectSettingType,
-
NumberSettingType,
-
SelectOption,
-
SelectSettingType
-
} from "@moonlight-mod/types/config";
-
import WebpackRequire from "@moonlight-mod/types/discord/require";
-
import { CircleXIconSVG, ExtensionState, MoonbaseExtension } from "../../types";
-
-
type SettingsProps = {
-
ext: MoonbaseExtension;
-
name: string;
-
setting: ExtensionSettingsManifest;
-
disabled: boolean;
-
};
-
-
type SettingsComponent = React.ComponentType<SettingsProps>;
-
-
export default (require: typeof WebpackRequire) => {
-
const React = require("common_react");
-
const CommonComponents = require("common_components");
-
const Flux = require("common_flux");
-
const spacepack = require("spacepack_spacepack").spacepack;
-
-
const { MoonbaseSettingsStore } =
-
require("moonbase_stores") as typeof import("../../webpackModules/stores");
-
-
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
-
-
function useConfigEntry<T>(id: string, name: string) {
-
return Flux.useStateFromStores(
-
[MoonbaseSettingsStore],
-
() => {
-
return {
-
value: MoonbaseSettingsStore.getExtensionConfig<T>(id, name),
-
displayName: MoonbaseSettingsStore.getExtensionConfigName(id, name),
-
description: MoonbaseSettingsStore.getExtensionConfigDescription(
-
id,
-
name
-
)
-
};
-
},
-
[id, name]
-
);
-
}
-
-
function Boolean({ ext, name, setting, disabled }: SettingsProps) {
-
const { FormSwitch } = CommonComponents;
-
const { value, displayName, description } = useConfigEntry<boolean>(
-
ext.id,
-
name
-
);
-
-
return (
-
<FormSwitch
-
value={value ?? false}
-
hideBorder={true}
-
disabled={disabled}
-
onChange={(value: boolean) => {
-
MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value);
-
}}
-
note={description}
-
className={`${Margins.marginReset} ${Margins.marginTop20}`}
-
>
-
{displayName}
-
</FormSwitch>
-
);
-
}
-
-
function Number({ ext, name, setting, disabled }: SettingsProps) {
-
const { FormItem, FormText, Slider } = CommonComponents;
-
const { value, displayName, description } = useConfigEntry<number>(
-
ext.id,
-
name
-
);
-
-
const castedSetting = setting as NumberSettingType;
-
const min = castedSetting.min ?? 0;
-
const max = castedSetting.max ?? 100;
-
-
return (
-
<FormItem className={Margins.marginTop20} title={displayName}>
-
{description && <FormText>{description}</FormText>}
-
<Slider
-
initialValue={value ?? 0}
-
disabled={disabled}
-
minValue={castedSetting.min ?? 0}
-
maxValue={castedSetting.max ?? 100}
-
onValueChange={(value: number) => {
-
const rounded = Math.max(min, Math.min(max, Math.round(value)));
-
MoonbaseSettingsStore.setExtensionConfig(ext.id, name, rounded);
-
}}
-
/>
-
</FormItem>
-
);
-
}
-
-
function String({ ext, name, setting, disabled }: SettingsProps) {
-
const { FormItem, FormText, TextInput } = CommonComponents;
-
const { value, displayName, description } = useConfigEntry<string>(
-
ext.id,
-
name
-
);
-
-
return (
-
<FormItem className={Margins.marginTop20} title={displayName}>
-
{description && (
-
<FormText className={Margins.marginBottom8}>{description}</FormText>
-
)}
-
<TextInput
-
value={value ?? ""}
-
onChange={(value: string) => {
-
if (disabled) return;
-
MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value);
-
}}
-
/>
-
</FormItem>
-
);
-
}
-
-
function Select({ ext, name, setting, disabled }: SettingsProps) {
-
const { FormItem, FormText, SingleSelect } = CommonComponents;
-
const { value, displayName, description } = useConfigEntry<string>(
-
ext.id,
-
name
-
);
-
-
const castedSetting = setting as SelectSettingType;
-
const options = castedSetting.options;
-
-
return (
-
<FormItem className={Margins.marginTop20} title={displayName}>
-
{description && (
-
<FormText className={Margins.marginBottom8}>{description}</FormText>
-
)}
-
<SingleSelect
-
autofocus={false}
-
clearable={false}
-
value={value ?? ""}
-
options={options.map((o: SelectOption) =>
-
typeof o === "string" ? { value: o, label: o } : o
-
)}
-
onChange={(value: string) => {
-
if (disabled) return;
-
MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value);
-
}}
-
/>
-
</FormItem>
-
);
-
}
-
-
function MultiSelect({ ext, name, setting, disabled }: SettingsProps) {
-
const { FormItem, FormText, Select, useVariableSelect, multiSelect } =
-
CommonComponents;
-
const { value, displayName, description } = useConfigEntry<
-
string | string[]
-
>(ext.id, name);
-
-
const castedSetting = setting as MultiSelectSettingType;
-
const options = castedSetting.options;
-
-
return (
-
<FormItem className={Margins.marginTop20} title={displayName}>
-
{description && (
-
<FormText className={Margins.marginBottom8}>{description}</FormText>
-
)}
-
<Select
-
autofocus={false}
-
clearable={false}
-
closeOnSelect={false}
-
options={options.map((o: SelectOption) =>
-
typeof o === "string" ? { value: o, label: o } : o
-
)}
-
{...useVariableSelect({
-
onSelectInteraction: multiSelect,
-
value: new Set(Array.isArray(value) ? value : [value]),
-
onChange: (value: string) => {
-
if (disabled) return;
-
MoonbaseSettingsStore.setExtensionConfig(
-
ext.id,
-
name,
-
Array.from(value)
-
);
-
}
-
})}
-
/>
-
</FormItem>
-
);
-
}
-
-
const RemoveButtonClasses = spacepack.findByCode("removeButtonContainer")[0]
-
.exports;
-
const CircleXIcon = spacepack.findByCode(CircleXIconSVG)[0].exports.default;
-
function RemoveEntryButton({
-
onClick,
-
disabled
-
}: {
-
onClick: () => void;
-
disabled: boolean;
-
}) {
-
const { Tooltip, Clickable } = CommonComponents;
-
return (
-
<div className={RemoveButtonClasses.removeButtonContainer}>
-
<Tooltip text="Remove entry" position="top">
-
{(props: any) => (
-
<Clickable
-
{...props}
-
className={RemoveButtonClasses.removeButton}
-
onClick={onClick}
-
>
-
<CircleXIcon width={16} height={16} />
-
</Clickable>
-
)}
-
</Tooltip>
-
</div>
-
);
-
}
-
-
function List({ ext, name, setting, disabled }: SettingsProps) {
-
const { FormItem, FormText, TextInput, Button, Flex } = CommonComponents;
-
const { value, displayName, description } = useConfigEntry<string[]>(
-
ext.id,
-
name
-
);
-
-
const entries = value ?? [];
-
const updateConfig = () =>
-
MoonbaseSettingsStore.setExtensionConfig(ext.id, name, entries);
-
-
return (
-
<FormItem className={Margins.marginTop20} title={displayName}>
-
{description && (
-
<FormText className={Margins.marginBottom4}>{description}</FormText>
-
)}
-
<Flex direction={Flex.Direction.VERTICAL}>
-
{entries.map((val, i) => (
-
// FIXME: stylesheets
-
<div
-
key={i}
-
style={{
-
display: "grid",
-
height: "32px",
-
gap: "8px",
-
gridTemplateColumns: "1fr 32px",
-
alignItems: "center"
-
}}
-
>
-
<TextInput
-
size={TextInput.Sizes.MINI}
-
value={val}
-
disabled={disabled}
-
onChange={(newVal: string) => {
-
entries[i] = newVal;
-
updateConfig();
-
}}
-
/>
-
<RemoveEntryButton
-
disabled={disabled}
-
onClick={() => {
-
entries.splice(i, 1);
-
updateConfig();
-
}}
-
/>
-
</div>
-
))}
-
-
<Button
-
look={Button.Looks.FILLED}
-
color={Button.Colors.GREEN}
-
size={Button.Sizes.SMALL}
-
disabled={disabled}
-
className={Margins.marginTop8}
-
onClick={() => {
-
entries.push("");
-
updateConfig();
-
}}
-
>
-
Add new entry
-
</Button>
-
</Flex>
-
</FormItem>
-
);
-
}
-
-
function Dictionary({ ext, name, setting, disabled }: SettingsProps) {
-
const { FormItem, FormText, TextInput, Button, Flex } = CommonComponents;
-
const { value, displayName, description } = useConfigEntry<
-
Record<string, string>
-
>(ext.id, name);
-
-
const entries = Object.entries(value ?? {});
-
const updateConfig = () =>
-
MoonbaseSettingsStore.setExtensionConfig(
-
ext.id,
-
name,
-
Object.fromEntries(entries)
-
);
-
-
return (
-
<FormItem className={Margins.marginTop20} title={displayName}>
-
{description && (
-
<FormText className={Margins.marginBottom4}>{description}</FormText>
-
)}
-
<Flex direction={Flex.Direction.VERTICAL}>
-
{entries.map(([key, val], i) => (
-
// FIXME: stylesheets
-
<div
-
key={i}
-
style={{
-
display: "grid",
-
height: "32px",
-
gap: "8px",
-
gridTemplateColumns: "1fr 1fr 32px",
-
alignItems: "center"
-
}}
-
>
-
<TextInput
-
size={TextInput.Sizes.MINI}
-
value={key}
-
disabled={disabled}
-
onChange={(newKey: string) => {
-
entries[i][0] = newKey;
-
updateConfig();
-
}}
-
/>
-
<TextInput
-
size={TextInput.Sizes.MINI}
-
value={val}
-
disabled={disabled}
-
onChange={(newValue: string) => {
-
entries[i][1] = newValue;
-
updateConfig();
-
}}
-
/>
-
<RemoveEntryButton
-
disabled={disabled}
-
onClick={() => {
-
entries.splice(i, 1);
-
updateConfig();
-
}}
-
/>
-
</div>
-
))}
-
-
<Button
-
look={Button.Looks.FILLED}
-
color={Button.Colors.GREEN}
-
size={Button.Sizes.SMALL}
-
className={Margins.marginTop8}
-
disabled={disabled}
-
onClick={() => {
-
entries.push([`entry-${entries.length}`, ""]);
-
updateConfig();
-
}}
-
>
-
Add new entry
-
</Button>
-
</Flex>
-
</FormItem>
-
);
-
}
-
-
function Setting({ ext, name, setting, disabled }: SettingsProps) {
-
const elements: Partial<Record<ExtensionSettingType, SettingsComponent>> = {
-
[ExtensionSettingType.Boolean]: Boolean,
-
[ExtensionSettingType.Number]: Number,
-
[ExtensionSettingType.String]: String,
-
[ExtensionSettingType.Select]: Select,
-
[ExtensionSettingType.MultiSelect]: MultiSelect,
-
[ExtensionSettingType.List]: List,
-
[ExtensionSettingType.Dictionary]: Dictionary
-
};
-
const element = elements[setting.type];
-
if (element == null) return <></>;
-
return React.createElement(element, { ext, name, setting, disabled });
-
}
-
-
return function Settings({ ext }: { ext: MoonbaseExtension }) {
-
const { Flex } = CommonComponents;
-
return (
-
<Flex className="moonbase-settings" direction={Flex.Direction.VERTICAL}>
-
{Object.entries(ext.manifest.settings!).map(([name, setting]) => (
-
<Setting
-
ext={ext}
-
key={name}
-
name={name}
-
setting={setting}
-
disabled={ext.state === ExtensionState.NotDownloaded}
-
/>
-
))}
-
</Flex>
-
);
-
};
-
};
-54
packages/core-extensions/src/moonbase/ui/index.tsx
···
-
import { WebpackRequireType } from "@moonlight-mod/types";
-
import { pageModules } from "..";
-
-
export default (require: WebpackRequireType) => {
-
const React = require("common_react");
-
const spacepack = require("spacepack_spacepack").spacepack;
-
-
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
-
-
const { Divider } = spacepack.findByCode(".default.HEADER_BAR")[0].exports
-
.default;
-
const TitleBarClasses = spacepack.findByCode("iconWrapper:", "children:")[0]
-
.exports;
-
const TabBarClasses = spacepack.findByCode("nowPlayingColumn:")[0].exports;
-
-
const pages = pageModules(require);
-
-
return function Moonbase() {
-
const { Text, TabBar } = require("common_components");
-
-
const [selectedTab, setSelectedTab] = React.useState(Object.keys(pages)[0]);
-
-
return (
-
<>
-
<div
-
className={`${TitleBarClasses.children} ${Margins.marginBottom20}`}
-
>
-
<Text
-
className={TitleBarClasses.titleWrapper}
-
variant="heading-lg/semibold"
-
tag="h2"
-
>
-
Moonbase
-
</Text>
-
<Divider />
-
<TabBar
-
selectedItem={selectedTab}
-
onItemSelect={setSelectedTab}
-
type="top-pill"
-
className={TabBarClasses.tabBar}
-
>
-
{Object.entries(pages).map(([id, page]) => (
-
<TabBar.Item key={id} id={id} className={TabBarClasses.item}>
-
{page.name}
-
</TabBar.Item>
-
))}
-
</TabBar>
-
</div>
-
-
{React.createElement(pages[selectedTab].element)}
-
</>
-
);
-
};
-
};
+51
packages/core-extensions/src/moonbase/webpackModules/moonbase.tsx
···
+
import settings from "@moonlight-mod/wp/settings_settings";
+
import React from "@moonlight-mod/wp/common_react";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
import { Moonbase, pages } from "@moonlight-mod/wp/moonbase_ui";
+
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
+
import { Text } from "@moonlight-mod/wp/common_components";
+
+
function addSection(name: string, element: React.FunctionComponent) {
+
settings.addSection(name, name, element, null, -2, {
+
stores: [MoonbaseSettingsStore],
+
element: () => {
+
// Require it here because lazy loading SUX
+
const SettingsNotice =
+
spacepack.findByCode("onSaveButtonColor")[0].exports.default;
+
return (
+
<SettingsNotice
+
submitting={MoonbaseSettingsStore.submitting}
+
onReset={() => {
+
MoonbaseSettingsStore.reset();
+
}}
+
onSave={() => {
+
MoonbaseSettingsStore.writeConfig();
+
}}
+
/>
+
);
+
}
+
});
+
}
+
+
if (moonlight.getConfigOption<boolean>("moonbase", "sections")) {
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
+
+
settings.addHeader("Moonbase", -2);
+
for (const page of Object.values(pages)) {
+
addSection(page.name, () => (
+
<>
+
<Text
+
className={Margins.marginBottom20}
+
variant="heading-lg/semibold"
+
tag="h2"
+
>
+
Extensions
+
</Text>
+
<page.element />
+
</>
+
));
+
}
+
} else {
+
addSection("Moonbase", Moonbase);
+
}
+92 -80
packages/core-extensions/src/moonbase/webpackModules/stores.ts
···
import { Config, ExtensionLoadSource } from "@moonlight-mod/types";
-
import {
-
ExtensionState,
-
MoonbaseExtension,
-
MoonbaseNatives,
-
RepositoryManifest
-
} from "../types";
+
import { ExtensionState, MoonbaseExtension, MoonbaseNatives } from "../types";
import Flux from "@moonlight-mod/wp/common_flux";
import Dispatcher from "@moonlight-mod/wp/common_fluxDispatcher";
···
class MoonbaseSettingsStore extends Flux.Store<any> {
private origConfig: Config;
private config: Config;
+
private extensionIndex: number;
modified: boolean;
submitting: boolean;
installing: boolean;
-
extensions: { [id: string]: MoonbaseExtension };
-
updates: { [id: string]: { version: string; download: string } };
+
extensions: { [id: number]: MoonbaseExtension };
+
updates: { [id: number]: { version: string; download: string } };
constructor() {
super(Dispatcher);
this.origConfig = moonlightNode.config;
this.config = this.clone(this.origConfig);
+
this.extensionIndex = 0;
this.modified = false;
this.submitting = false;
···
this.extensions = {};
this.updates = {};
for (const ext of moonlightNode.extensions) {
-
const existingExtension = this.extensions[ext.id];
-
if (existingExtension != null) continue;
-
-
this.extensions[ext.id] = {
+
const uniqueId = this.extensionIndex++;
+
this.extensions[uniqueId] = {
...ext,
+
uniqueId,
state: moonlight.enabledExtensions.has(ext.id)
? ExtensionState.Enabled
: ExtensionState.Disabled
···
for (const [repo, exts] of Object.entries(ret)) {
try {
for (const ext of exts) {
-
try {
-
const existingExtension = this.extensions[ext.id];
-
if (existingExtension !== undefined) {
-
if (this.hasUpdate(repo, ext, existingExtension)) {
-
this.updates[ext.id] = {
-
version: ext.version!,
-
download: ext.download
-
};
-
}
+
const uniqueId = this.extensionIndex++;
+
const extensionData = {
+
id: ext.id,
+
uniqueId,
+
manifest: ext,
+
source: { type: ExtensionLoadSource.Normal, url: repo },
+
state: ExtensionState.NotDownloaded
+
};
-
this.extensions[ext.id].manifest = ext;
-
this.extensions[ext.id].source = {
-
type: ExtensionLoadSource.Normal,
-
url: repo
+
if (this.alreadyExists(extensionData)) {
+
if (this.hasUpdate(extensionData)) {
+
this.updates[uniqueId] = {
+
version: ext.version!,
+
download: ext.download
};
-
-
continue;
}
-
this.extensions[ext.id] = {
-
id: ext.id,
-
manifest: ext,
-
source: { type: ExtensionLoadSource.Normal, url: repo },
-
state: ExtensionState.NotDownloaded
-
};
-
} catch (e) {
-
logger.error(`Error processing extension ${ext.id}`, e);
+
continue;
}
+
+
this.extensions[uniqueId] = extensionData;
}
} catch (e) {
logger.error(`Error processing repository ${repo}`, e);
···
});
}
-
// this logic sucks so bad lol
-
private hasUpdate(
-
repo: string,
-
repoExt: RepositoryManifest,
-
existing: MoonbaseExtension
-
) {
+
private alreadyExists(ext: MoonbaseExtension) {
+
return Object.values(this.extensions).some(
+
(e) => e.id === ext.id && e.source.url === ext.source.url
+
);
+
}
+
+
private hasUpdate(ext: MoonbaseExtension) {
+
const existing = Object.values(this.extensions).find(
+
(e) => e.id === ext.id && e.source.url === ext.source.url
+
);
+
if (existing == null) return false;
+
return (
-
existing.source.type === ExtensionLoadSource.Normal &&
-
existing.source.url != null &&
-
existing.source.url === repo &&
-
repoExt.version != null &&
-
existing.manifest.version !== repoExt.version
+
existing.manifest.version !== ext.manifest.version &&
+
existing.state !== ExtensionState.NotDownloaded
);
}
···
return this.modified;
}
-
getExtension(id: string) {
-
return this.extensions[id];
+
getExtension(uniqueId: number) {
+
return this.extensions[uniqueId];
}
-
getExtensionName(id: string) {
-
return Object.prototype.hasOwnProperty.call(this.extensions, id)
-
? this.extensions[id].manifest.meta?.name ?? id
-
: id;
+
getExtensionUniqueId(id: string) {
+
return Object.values(this.extensions).find((ext) => ext.id === id)
+
?.uniqueId;
+
}
+
+
getExtensionConflicting(uniqueId: number) {
+
const ext = this.getExtension(uniqueId);
+
if (ext.state !== ExtensionState.NotDownloaded) return false;
+
return Object.values(this.extensions).some(
+
(e) =>
+
e.id === ext.id &&
+
e.uniqueId !== uniqueId &&
+
e.state !== ExtensionState.NotDownloaded
+
);
+
}
+
+
getExtensionName(uniqueId: number) {
+
const ext = this.getExtension(uniqueId);
+
return ext.manifest.meta?.name ?? ext.id;
}
-
getExtensionUpdate(id: string) {
-
return Object.prototype.hasOwnProperty.call(this.updates, id)
-
? this.updates[id]
-
: null;
+
getExtensionUpdate(uniqueId: number) {
+
return this.updates[uniqueId]?.version;
}
-
getExtensionEnabled(id: string) {
-
const val = this.config.extensions[id];
+
getExtensionEnabled(uniqueId: number) {
+
const ext = this.getExtension(uniqueId);
+
if (ext.state === ExtensionState.NotDownloaded) return false;
+
const val = this.config.extensions[ext.id];
if (val == null) return false;
return typeof val === "boolean" ? val : val.enabled;
}
-
getExtensionConfig<T>(id: string, key: string): T | undefined {
-
const defaultValue = this.extensions[id].manifest.settings?.[key]?.default;
+
getExtensionConfig<T>(uniqueId: number, key: string): T | undefined {
+
const ext = this.getExtension(uniqueId);
+
const defaultValue = ext.manifest.settings?.[key]?.default;
const clonedDefaultValue = this.clone(defaultValue);
-
const cfg = this.config.extensions[id];
+
const cfg = this.config.extensions[ext.id];
if (cfg == null || typeof cfg === "boolean") return clonedDefaultValue;
return cfg.config?.[key] ?? clonedDefaultValue;
}
-
getExtensionConfigName(id: string, key: string) {
-
return this.extensions[id].manifest.settings?.[key]?.displayName ?? key;
+
getExtensionConfigName(uniqueId: number, key: string) {
+
const ext = this.getExtension(uniqueId);
+
return ext.manifest.settings?.[key]?.displayName ?? key;
}
-
getExtensionConfigDescription(id: string, key: string) {
-
return this.extensions[id].manifest.settings?.[key]?.description;
+
getExtensionConfigDescription(uniqueId: number, key: string) {
+
const ext = this.getExtension(uniqueId);
+
return ext.manifest.settings?.[key]?.description;
}
-
setExtensionConfig(id: string, key: string, value: any) {
-
const oldConfig = this.config.extensions[id];
+
setExtensionConfig(uniqueId: number, key: string, value: any) {
+
const ext = this.getExtension(uniqueId);
+
const oldConfig = this.config.extensions[ext.id];
const newConfig =
typeof oldConfig === "boolean"
? {
···
config: { ...(oldConfig?.config ?? {}), [key]: value }
};
-
this.config.extensions[id] = newConfig;
+
this.config.extensions[ext.id] = newConfig;
this.modified = this.isModified();
this.emitChange();
}
-
setExtensionEnabled(id: string, enabled: boolean) {
-
let val = this.config.extensions[id];
+
setExtensionEnabled(uniqueId: number, enabled: boolean) {
+
const ext = this.getExtension(uniqueId);
+
let val = this.config.extensions[ext.id];
if (val == null) {
-
this.config.extensions[id] = { enabled };
+
this.config.extensions[ext.id] = { enabled };
this.modified = this.isModified();
this.emitChange();
return;
···
val.enabled = enabled;
}
-
this.config.extensions[id] = val;
+
this.config.extensions[ext.id] = val;
this.modified = this.isModified();
this.emitChange();
}
-
async installExtension(id: string) {
-
const ext = this.getExtension(id);
+
async installExtension(uniqueId: number) {
+
const ext = this.getExtension(uniqueId);
if (!("download" in ext.manifest)) {
throw new Error("Extension has no download URL");
}
this.installing = true;
try {
-
const url = this.updates[id]?.download ?? ext.manifest.download;
+
const url = this.updates[uniqueId]?.download ?? ext.manifest.download;
await natives.installExtension(ext.manifest, url, ext.source.url!);
if (ext.state === ExtensionState.NotDownloaded) {
-
this.extensions[id].state = ExtensionState.Disabled;
+
this.extensions[uniqueId].state = ExtensionState.Disabled;
}
-
delete this.updates[id];
+
delete this.updates[uniqueId];
} catch (e) {
logger.error("Error installing extension:", e);
}
···
this.emitChange();
}
-
async deleteExtension(id: string) {
-
const ext = this.getExtension(id);
+
async deleteExtension(uniqueId: number) {
+
const ext = this.getExtension(uniqueId);
if (ext == null) return;
this.installing = true;
try {
await natives.deleteExtension(ext.id);
-
this.extensions[id].state = ExtensionState.NotDownloaded;
+
this.extensions[uniqueId].state = ExtensionState.NotDownloaded;
} catch (e) {
logger.error("Error deleting extension:", e);
}
+151
packages/core-extensions/src/moonbase/webpackModules/ui/config/index.tsx
···
+
import { LogLevel } from "@moonlight-mod/types";
+
import { CircleXIconSVG } from "../../../types";
+
+
const logLevels = Object.values(LogLevel).filter(
+
(v) => typeof v === "string"
+
) as string[];
+
+
import React from "@moonlight-mod/wp/common_react";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
import {
+
FormDivider,
+
FormItem,
+
FormText,
+
FormSwitch,
+
TextInput,
+
Flex,
+
Button,
+
SingleSelect,
+
Tooltip,
+
Clickable
+
} from "@moonlight-mod/wp/common_components";
+
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
+
+
const FormClasses = spacepack.findByCode("dividerDefault:")[0].exports;
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
+
+
const RemoveButtonClasses = spacepack.findByCode("removeButtonContainer")[0]
+
.exports;
+
const CircleXIcon = spacepack.findByCode(CircleXIconSVG)[0].exports.default;
+
function RemoveEntryButton({ onClick }: { onClick: () => void }) {
+
return (
+
<div className={RemoveButtonClasses.removeButtonContainer}>
+
<Tooltip text="Remove entry" position="top">
+
{(props: any) => (
+
<Clickable
+
{...props}
+
className={RemoveButtonClasses.removeButton}
+
onClick={onClick}
+
>
+
<CircleXIcon width={24} height={24} />
+
</Clickable>
+
)}
+
</Tooltip>
+
</div>
+
);
+
}
+
+
function ArrayFormItem({
+
config
+
}: {
+
config: "repositories" | "devSearchPaths";
+
}) {
+
const items = MoonbaseSettingsStore.getConfigOption(config) ?? [];
+
return (
+
<Flex
+
style={{
+
gap: "20px"
+
}}
+
direction={Flex.Direction.VERTICAL}
+
>
+
{items.map((val, i) => (
+
<div
+
key={i}
+
style={{
+
display: "grid",
+
height: "32px",
+
gap: "8px",
+
gridTemplateColumns: "1fr 32px",
+
alignItems: "center"
+
}}
+
>
+
<TextInput
+
size={TextInput.Sizes.DEFAULT}
+
value={val}
+
onChange={(newVal: string) => {
+
items[i] = newVal;
+
MoonbaseSettingsStore.setConfigOption(config, items);
+
}}
+
/>
+
<RemoveEntryButton
+
onClick={() => {
+
items.splice(i, 1);
+
MoonbaseSettingsStore.setConfigOption(config, items);
+
}}
+
/>
+
</div>
+
))}
+
+
<Button
+
look={Button.Looks.FILLED}
+
color={Button.Colors.GREEN}
+
size={Button.Sizes.SMALL}
+
style={{
+
marginTop: "10px"
+
}}
+
onClick={() => {
+
items.push("");
+
MoonbaseSettingsStore.setConfigOption(config, items);
+
}}
+
>
+
Add new entry
+
</Button>
+
</Flex>
+
);
+
}
+
+
export default function ConfigPage() {
+
return (
+
<>
+
<FormItem title="Repositories">
+
<FormText className={Margins.marginBottom4}>
+
A list of remote repositories to display extensions from
+
</FormText>
+
<ArrayFormItem config="repositories" />
+
</FormItem>
+
<FormDivider className={FormClasses.dividerDefault} />
+
<FormItem title="Extension search paths" className={Margins.marginTop20}>
+
<FormText className={Margins.marginBottom4}>
+
A list of local directories to search for built extensions
+
</FormText>
+
<ArrayFormItem config="devSearchPaths" />
+
</FormItem>
+
<FormDivider className={FormClasses.dividerDefault} />
+
<FormSwitch
+
className={Margins.marginTop20}
+
value={MoonbaseSettingsStore.getConfigOption("patchAll")}
+
onChange={(value: boolean) => {
+
MoonbaseSettingsStore.setConfigOption("patchAll", value);
+
}}
+
note="Wraps every webpack module in a function, separating them in DevTools"
+
>
+
Patch all
+
</FormSwitch>
+
<FormItem title="Log level">
+
<SingleSelect
+
autofocus={false}
+
clearable={false}
+
value={MoonbaseSettingsStore.getConfigOption("loggerLevel")}
+
options={logLevels.map((o) => ({
+
value: o.toLowerCase(),
+
label: o[0] + o.slice(1).toLowerCase()
+
}))}
+
onChange={(v) =>
+
MoonbaseSettingsStore.setConfigOption("loggerLevel", v)
+
}
+
/>
+
</FormItem>
+
</>
+
);
+
}
+220
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/card.tsx
···
+
import {
+
DangerIconSVG,
+
DownloadIconSVG,
+
ExtensionState,
+
TrashIconSVG
+
} from "../../../types";
+
import { ExtensionLoadSource } from "@moonlight-mod/types";
+
+
import React from "@moonlight-mod/wp/common_react";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
import CommonComponents from "@moonlight-mod/wp/common_components";
+
import * as Flux from "@moonlight-mod/wp/common_flux";
+
+
import ExtensionInfo from "./info";
+
import Settings from "./settings";
+
+
export enum ExtensionPage {
+
Info,
+
Description,
+
Settings
+
}
+
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
+
+
const UserProfileClasses = spacepack.findByCode(
+
"tabBarContainer",
+
"topSection"
+
)[0].exports;
+
+
const DownloadIcon =
+
spacepack.findByCode(DownloadIconSVG)[0].exports.DownloadIcon;
+
const TrashIcon = spacepack.findByCode(TrashIconSVG)[0].exports.default;
+
const DangerIcon =
+
spacepack.findByCode(DangerIconSVG)[0].exports.CircleExclamationPointIcon;
+
+
const PanelButton =
+
spacepack.findByCode("Masks.PANEL_BUTTON")[0].exports.default;
+
+
export default function ExtensionCard({ uniqueId }: { uniqueId: number }) {
+
const [tab, setTab] = React.useState(ExtensionPage.Info);
+
const [restartNeeded, setRestartNeeded] = React.useState(false);
+
+
const { ext, enabled, busy, update, conflicting } = Flux.useStateFromStores(
+
[MoonbaseSettingsStore],
+
() => {
+
return {
+
ext: MoonbaseSettingsStore.getExtension(uniqueId),
+
enabled: MoonbaseSettingsStore.getExtensionEnabled(uniqueId),
+
busy: MoonbaseSettingsStore.busy,
+
update: MoonbaseSettingsStore.getExtensionUpdate(uniqueId),
+
conflicting: MoonbaseSettingsStore.getExtensionConflicting(uniqueId)
+
};
+
}
+
);
+
+
// Why it work like that :sob:
+
if (ext == null) return <></>;
+
+
const {
+
Card,
+
CardClasses,
+
Flex,
+
Text,
+
MarkdownParser,
+
Switch,
+
TabBar,
+
Button
+
} = CommonComponents;
+
+
const tagline = ext.manifest?.meta?.tagline;
+
const settings = ext.manifest?.settings;
+
const description = ext.manifest?.meta?.description;
+
+
return (
+
<Card editable={true} className={CardClasses.card}>
+
<div className={CardClasses.cardHeader}>
+
<Flex direction={Flex.Direction.VERTICAL}>
+
<Flex direction={Flex.Direction.HORIZONTAL}>
+
<Text variant="text-md/semibold">
+
{ext.manifest?.meta?.name ?? ext.id}
+
</Text>
+
</Flex>
+
+
{tagline != null && (
+
<Text variant="text-sm/normal">
+
{MarkdownParser.parse(tagline)}
+
</Text>
+
)}
+
</Flex>
+
+
<Flex
+
direction={Flex.Direction.HORIZONTAL}
+
align={Flex.Align.END}
+
justify={Flex.Justify.END}
+
>
+
{ext.state === ExtensionState.NotDownloaded ? (
+
<Button
+
color={Button.Colors.BRAND}
+
submitting={busy}
+
disabled={conflicting}
+
onClick={() => {
+
MoonbaseSettingsStore.installExtension(uniqueId);
+
}}
+
>
+
Install
+
</Button>
+
) : (
+
<div
+
// too lazy to learn how <Flex /> works lmao
+
style={{
+
display: "flex",
+
alignItems: "center",
+
gap: "1rem"
+
}}
+
>
+
{ext.source.type === ExtensionLoadSource.Normal && (
+
<PanelButton
+
icon={TrashIcon}
+
tooltipText="Delete"
+
onClick={() => {
+
MoonbaseSettingsStore.deleteExtension(uniqueId);
+
}}
+
/>
+
)}
+
+
{update != null && (
+
<PanelButton
+
icon={DownloadIcon}
+
tooltipText="Update"
+
onClick={() => {
+
MoonbaseSettingsStore.installExtension(uniqueId);
+
}}
+
/>
+
)}
+
+
{restartNeeded && (
+
<PanelButton
+
icon={() => (
+
<DangerIcon
+
color={CommonComponents.tokens.colors.STATUS_DANGER}
+
/>
+
)}
+
onClick={() => window.location.reload()}
+
tooltipText="You will need to reload/restart your client for this extension to work properly."
+
/>
+
)}
+
+
<Switch
+
checked={enabled}
+
onChange={() => {
+
setRestartNeeded(true);
+
MoonbaseSettingsStore.setExtensionEnabled(uniqueId, !enabled);
+
}}
+
/>
+
</div>
+
)}
+
</Flex>
+
</div>
+
+
<div className={UserProfileClasses.body}>
+
{(description != null || settings != null) && (
+
<div
+
className={UserProfileClasses.tabBarContainer}
+
style={{
+
padding: "0 10px"
+
}}
+
>
+
<TabBar
+
selectedItem={tab}
+
type="top"
+
onItemSelect={setTab}
+
className={UserProfileClasses.tabBar}
+
>
+
<TabBar.Item
+
className={UserProfileClasses.tabBarItem}
+
id={ExtensionPage.Info}
+
>
+
Info
+
</TabBar.Item>
+
+
{description != null && (
+
<TabBar.Item
+
className={UserProfileClasses.tabBarItem}
+
id={ExtensionPage.Description}
+
>
+
Description
+
</TabBar.Item>
+
)}
+
+
{settings != null && (
+
<TabBar.Item
+
className={UserProfileClasses.tabBarItem}
+
id={ExtensionPage.Settings}
+
>
+
Settings
+
</TabBar.Item>
+
)}
+
</TabBar>
+
</div>
+
)}
+
+
<Flex
+
justify={Flex.Justify.START}
+
wrap={Flex.Wrap.WRAP}
+
style={{
+
padding: "16px 16px"
+
}}
+
>
+
{tab === ExtensionPage.Info && <ExtensionInfo ext={ext} />}
+
{tab === ExtensionPage.Description && (
+
<Text variant="text-md/normal">
+
{MarkdownParser.parse(description ?? "*No description*")}
+
</Text>
+
)}
+
{tab === ExtensionPage.Settings && <Settings ext={ext} />}
+
</Flex>
+
</div>
+
</Card>
+
);
+
}
+370
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/filterBar.tsx
···
+
import { tagNames } from "./info";
+
import {
+
ArrowsUpDownIconSVG,
+
ChevronSmallDownIconSVG,
+
ChevronSmallUpIconSVG
+
} from "../../../types";
+
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
import React from "@moonlight-mod/wp/common_react";
+
import * as Flux from "@moonlight-mod/wp/common_flux";
+
import { WindowStore } from "@moonlight-mod/wp/common_stores";
+
import {
+
Button,
+
Text,
+
Heading,
+
Popout,
+
Dialog,
+
Menu,
+
MenuGroup,
+
MenuCheckboxItem,
+
MenuItem
+
} from "@moonlight-mod/wp/common_components";
+
+
export enum Filter {
+
Core = 1 << 0,
+
Normal = 1 << 1,
+
Developer = 1 << 2,
+
Enabled = 1 << 3,
+
Disabled = 1 << 4,
+
Installed = 1 << 5,
+
Repository = 1 << 6
+
}
+
export const defaultFilter = ~(~0 << 7);
+
+
const modPromise = spacepack.lazyLoad(
+
'"Missing channel in Channel.openChannelContextMenu"',
+
/e\("(\d+)"\)/g,
+
/webpackId:"(.+?)"/
+
);
+
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
+
const SortMenuClasses = spacepack.findByCode("container:", "clearText:")[0]
+
.exports;
+
const FilterDialogClasses = spacepack.findByCode(
+
"countContainer:",
+
"tagContainer:"
+
)[0].exports;
+
const FilterBarClasses = spacepack.findByCode("tagsButtonWithCount:")[0]
+
.exports;
+
+
const TagItem = spacepack.findByCode(".FORUM_TAG_A11Y_FILTER_BY_TAG")[0].exports
+
.default;
+
+
const ChevronSmallDownIcon = spacepack.findByCode(ChevronSmallDownIconSVG)[0]
+
.exports.default;
+
const ChevronSmallUpIcon = spacepack.findByCode(ChevronSmallUpIconSVG)[0]
+
.exports.default;
+
let ArrowsUpDownIcon: React.FunctionComponent;
+
+
function toggleTag(
+
selectedTags: Set<string>,
+
setSelectedTags: (tags: Set<string>) => void,
+
tag: string
+
) {
+
const newState = new Set(selectedTags);
+
if (newState.has(tag)) newState.delete(tag);
+
else newState.add(tag);
+
setSelectedTags(newState);
+
}
+
+
function FilterButtonPopout({
+
filter,
+
setFilter,
+
closePopout
+
}: {
+
filter: Filter;
+
setFilter: (filter: Filter) => void;
+
closePopout: () => void;
+
}) {
+
const toggleFilter = (set: Filter) =>
+
setFilter(filter & set ? filter & ~set : filter | set);
+
+
return (
+
<div className={SortMenuClasses.container}>
+
<Menu navId="sort-filter" hideScrollbar={true} onClose={closePopout}>
+
<MenuGroup label="Type">
+
<MenuCheckboxItem
+
id="t-core"
+
label="Core"
+
checked={filter & Filter.Core}
+
action={() => toggleFilter(Filter.Core)}
+
/>
+
<MenuCheckboxItem
+
id="t-normal"
+
label="Normal"
+
checked={filter & Filter.Normal}
+
action={() => toggleFilter(Filter.Normal)}
+
/>
+
<MenuCheckboxItem
+
id="t-developer"
+
label="Developer"
+
checked={filter & Filter.Developer}
+
action={() => toggleFilter(Filter.Developer)}
+
/>
+
</MenuGroup>
+
<MenuGroup label="State">
+
<MenuCheckboxItem
+
id="s-enabled"
+
label="Enabled"
+
checked={filter & Filter.Enabled}
+
action={() => toggleFilter(Filter.Enabled)}
+
/>
+
<MenuCheckboxItem
+
id="s-disabled"
+
label="Disabled"
+
checked={filter & Filter.Disabled}
+
action={() => toggleFilter(Filter.Disabled)}
+
/>
+
</MenuGroup>
+
<MenuGroup label="Location">
+
<MenuCheckboxItem
+
id="l-installed"
+
label="Installed"
+
checked={filter & Filter.Installed}
+
action={() => toggleFilter(Filter.Installed)}
+
/>
+
<MenuCheckboxItem
+
id="l-repository"
+
label="Repository"
+
checked={filter & Filter.Repository}
+
action={() => toggleFilter(Filter.Repository)}
+
/>
+
</MenuGroup>
+
<MenuGroup>
+
<MenuItem
+
id="reset-all"
+
className={SortMenuClasses.clearText}
+
label={
+
<Text variant="text-sm/medium" color="none">
+
Reset to default
+
</Text>
+
}
+
action={() => {
+
setFilter(defaultFilter);
+
closePopout();
+
}}
+
/>
+
</MenuGroup>
+
</Menu>
+
</div>
+
);
+
}
+
+
function TagButtonPopout({
+
selectedTags,
+
setSelectedTags,
+
setPopoutRef,
+
closePopout
+
}: any) {
+
return (
+
<Dialog ref={setPopoutRef} className={FilterDialogClasses.container}>
+
<div className={FilterDialogClasses.header}>
+
<div className={FilterDialogClasses.headerLeft}>
+
<Heading
+
color="interactive-normal"
+
variant="text-xs/bold"
+
className={FilterDialogClasses.headerText}
+
>
+
Select tags
+
</Heading>
+
<div className={FilterDialogClasses.countContainer}>
+
<Text
+
className={FilterDialogClasses.countText}
+
color="none"
+
variant="text-xs/medium"
+
>
+
{selectedTags.size}
+
</Text>
+
</div>
+
</div>
+
</div>
+
<div className={FilterDialogClasses.tagContainer}>
+
{Object.keys(tagNames).map((tag) => (
+
<TagItem
+
key={tag}
+
className={FilterDialogClasses.tag}
+
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
+
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
+
selected={selectedTags.has(tag)}
+
/>
+
))}
+
</div>
+
<div className={FilterDialogClasses.separator} />
+
<Button
+
look={Button.Looks.LINK}
+
size={Button.Sizes.MIN}
+
color={Button.Colors.CUSTOM}
+
className={FilterDialogClasses.clear}
+
onClick={() => {
+
setSelectedTags(new Set());
+
closePopout();
+
}}
+
>
+
<Text variant="text-sm/medium" color="text-link">
+
Clear all
+
</Text>
+
</Button>
+
</Dialog>
+
);
+
}
+
+
function FilterBar({
+
filter,
+
setFilter,
+
selectedTags,
+
setSelectedTags
+
}: {
+
filter: Filter;
+
setFilter: (filter: Filter) => void;
+
selectedTags: Set<string>;
+
setSelectedTags: (tags: Set<string>) => void;
+
}) {
+
const windowSize = Flux.useStateFromStores([WindowStore], () =>
+
WindowStore.windowSize()
+
);
+
+
const tagsContainer = React.useRef<HTMLDivElement>(null);
+
const tagListInner = React.useRef<HTMLDivElement>(null);
+
const [tagsButtonOffset, setTagsButtonOffset] = React.useState(0);
+
React.useLayoutEffect(() => {
+
if (tagsContainer.current === null || tagListInner.current === null) return;
+
const { left: containerX, top: containerY } =
+
tagsContainer.current.getBoundingClientRect();
+
let offset = 0;
+
for (const child of tagListInner.current.children) {
+
const {
+
right: childX,
+
top: childY,
+
height
+
} = child.getBoundingClientRect();
+
if (childY - containerY > height) break;
+
const newOffset = childX - containerX;
+
if (newOffset > offset) {
+
offset = newOffset;
+
}
+
}
+
setTagsButtonOffset(offset);
+
}, [windowSize]);
+
+
return (
+
<div
+
ref={tagsContainer}
+
style={{
+
paddingTop: "12px"
+
}}
+
className={`${FilterBarClasses.tagsContainer} ${Margins.marginBottom8}`}
+
>
+
<Popout
+
renderPopout={({ closePopout }: any) => (
+
<FilterButtonPopout
+
filter={filter}
+
setFilter={setFilter}
+
closePopout={closePopout}
+
/>
+
)}
+
position="bottom"
+
align="left"
+
>
+
{(props: any, { isShown }: { isShown: boolean }) => (
+
<Button
+
{...props}
+
size={Button.Sizes.MIN}
+
color={Button.Colors.CUSTOM}
+
className={FilterBarClasses.sortDropdown}
+
innerClassName={FilterBarClasses.sortDropdownInner}
+
>
+
<ArrowsUpDownIcon />
+
<Text
+
className={FilterBarClasses.sortDropdownText}
+
variant="text-sm/medium"
+
color="interactive-normal"
+
>
+
Sort & filter
+
</Text>
+
{isShown ? (
+
<ChevronSmallUpIcon size={20} />
+
) : (
+
<ChevronSmallDownIcon size={20} />
+
)}
+
</Button>
+
)}
+
</Popout>
+
<div className={FilterBarClasses.divider} />
+
<div className={FilterBarClasses.tagList}>
+
<div ref={tagListInner} className={FilterBarClasses.tagListInner}>
+
{Object.keys(tagNames).map((tag) => (
+
<TagItem
+
key={tag}
+
className={FilterBarClasses.tag}
+
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
+
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
+
selected={selectedTags.has(tag)}
+
/>
+
))}
+
</div>
+
</div>
+
<Popout
+
renderPopout={({ setPopoutRef, closePopout }: any) => (
+
<TagButtonPopout
+
selectedTags={selectedTags}
+
setSelectedTags={setSelectedTags}
+
setPopoutRef={setPopoutRef}
+
closePopout={closePopout}
+
/>
+
)}
+
position="bottom"
+
align="right"
+
>
+
{(props: any, { isShown }: { isShown: boolean }) => (
+
<Button
+
{...props}
+
size={Button.Sizes.MIN}
+
color={Button.Colors.CUSTOM}
+
style={{
+
left: tagsButtonOffset
+
}}
+
// TODO: Use Discord's class name utility
+
className={`${FilterBarClasses.tagsButton} ${
+
selectedTags.size > 0 ? FilterBarClasses.tagsButtonWithCount : ""
+
}`}
+
innerClassName={FilterBarClasses.tagsButtonInner}
+
>
+
{selectedTags.size > 0 ? (
+
<div
+
style={{ boxSizing: "content-box" }}
+
className={FilterBarClasses.countContainer}
+
>
+
<Text
+
className={FilterBarClasses.countText}
+
color="none"
+
variant="text-xs/medium"
+
>
+
{selectedTags.size}
+
</Text>
+
</div>
+
) : (
+
<>All</>
+
)}
+
{isShown ? (
+
<ChevronSmallUpIcon size={20} />
+
) : (
+
<ChevronSmallDownIcon size={20} />
+
)}
+
</Button>
+
)}
+
</Popout>
+
</div>
+
);
+
}
+
+
// TODO: spacepack lazy loading utils
+
export default React.lazy(() =>
+
modPromise.then(async () => {
+
await modPromise;
+
ArrowsUpDownIcon ??=
+
spacepack.findByCode(ArrowsUpDownIconSVG)[0].exports.default;
+
+
return { default: FilterBar };
+
})
+
);
+105
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/index.tsx
···
+
import { ExtensionLoadSource, ExtensionTag } from "@moonlight-mod/types";
+
import { ExtensionState } from "../../../types";
+
import FilterBar, { Filter, defaultFilter } from "./filterBar";
+
import ExtensionCard from "./card";
+
+
import React from "@moonlight-mod/wp/common_react";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
import * as Flux from "@moonlight-mod/wp/common_flux";
+
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
+
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
+
const SearchBar = spacepack.findByCode("Messages.SEARCH", "hideSearchIcon")[0]
+
.exports.default;
+
+
export default function ExtensionsPage() {
+
const moonbaseId = MoonbaseSettingsStore.getExtensionUniqueId("moonbase")!;
+
const { extensions, savedFilter } = Flux.useStateFromStoresObject(
+
[MoonbaseSettingsStore],
+
() => {
+
return {
+
extensions: MoonbaseSettingsStore.extensions,
+
savedFilter: MoonbaseSettingsStore.getExtensionConfig(
+
moonbaseId,
+
"filter"
+
)
+
};
+
}
+
);
+
+
const [query, setQuery] = React.useState("");
+
+
let filter: Filter, setFilter: (filter: Filter) => void;
+
if (moonlight.getConfigOption<boolean>("moonbase", "saveFilter")) {
+
filter = savedFilter ?? defaultFilter;
+
setFilter = (filter) =>
+
MoonbaseSettingsStore.setExtensionConfig(moonbaseId, "filter", filter);
+
} else {
+
const state = React.useState(defaultFilter);
+
filter = state[0];
+
setFilter = state[1];
+
}
+
const [selectedTags, setSelectedTags] = React.useState(new Set<string>());
+
const sorted = Object.values(extensions).sort((a, b) => {
+
const aName = a.manifest.meta?.name ?? a.id;
+
const bName = b.manifest.meta?.name ?? b.id;
+
return aName.localeCompare(bName);
+
});
+
+
const filtered = sorted.filter(
+
(ext) =>
+
(ext.manifest.meta?.name?.toLowerCase().includes(query) ||
+
ext.manifest.meta?.tagline?.toLowerCase().includes(query) ||
+
ext.manifest.meta?.description?.toLowerCase().includes(query)) &&
+
[...selectedTags.values()].every(
+
(tag) => ext.manifest.meta?.tags?.includes(tag as ExtensionTag)
+
) &&
+
// This seems very bad, sorry
+
!(
+
(!(filter & Filter.Core) &&
+
ext.source.type === ExtensionLoadSource.Core) ||
+
(!(filter & Filter.Normal) &&
+
ext.source.type === ExtensionLoadSource.Normal) ||
+
(!(filter & Filter.Developer) &&
+
ext.source.type === ExtensionLoadSource.Developer) ||
+
(!(filter & Filter.Enabled) &&
+
MoonbaseSettingsStore.getExtensionEnabled(ext.uniqueId)) ||
+
(!(filter & Filter.Disabled) &&
+
!MoonbaseSettingsStore.getExtensionEnabled(ext.uniqueId)) ||
+
(!(filter & Filter.Installed) &&
+
ext.state !== ExtensionState.NotDownloaded) ||
+
(!(filter & Filter.Repository) &&
+
ext.state === ExtensionState.NotDownloaded)
+
)
+
);
+
+
return (
+
<>
+
<SearchBar
+
size={SearchBar.Sizes.MEDIUM}
+
query={query}
+
onChange={(v: string) => setQuery(v.toLowerCase())}
+
onClear={() => setQuery("")}
+
autoFocus={true}
+
autoComplete="off"
+
inputProps={{
+
autoCapitalize: "none",
+
autoCorrect: "off",
+
spellCheck: "false"
+
}}
+
/>
+
<React.Suspense fallback={<div className={Margins.marginBottom20}></div>}>
+
<FilterBar
+
filter={filter}
+
setFilter={setFilter}
+
selectedTags={selectedTags}
+
setSelectedTags={setSelectedTags}
+
/>
+
</React.Suspense>
+
{filtered.map((ext) => (
+
<ExtensionCard uniqueId={ext.uniqueId} key={ext.id} />
+
))}
+
</>
+
);
+
}
+204
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/info.tsx
···
+
import { ExtensionTag } from "@moonlight-mod/types";
+
import { MoonbaseExtension } from "../../../types";
+
+
import React from "@moonlight-mod/wp/common_react";
+
import CommonComponents from "@moonlight-mod/wp/common_components";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
+
type Dependency = {
+
id: string;
+
type: DependencyType;
+
};
+
+
enum DependencyType {
+
Dependency = "dependency",
+
Optional = "optional",
+
Incompatible = "incompatible"
+
}
+
+
export const tagNames: Record<ExtensionTag, string> = {
+
[ExtensionTag.Accessibility]: "Accessibility",
+
[ExtensionTag.Appearance]: "Appearance",
+
[ExtensionTag.Chat]: "Chat",
+
[ExtensionTag.Commands]: "Commands",
+
[ExtensionTag.ContextMenu]: "Context Menu",
+
[ExtensionTag.DangerZone]: "Danger Zone",
+
[ExtensionTag.Development]: "Development",
+
[ExtensionTag.Fixes]: "Fixes",
+
[ExtensionTag.Fun]: "Fun",
+
[ExtensionTag.Markdown]: "Markdown",
+
[ExtensionTag.Voice]: "Voice",
+
[ExtensionTag.Privacy]: "Privacy",
+
[ExtensionTag.Profiles]: "Profiles",
+
[ExtensionTag.QualityOfLife]: "Quality of Life",
+
[ExtensionTag.Library]: "Library"
+
};
+
+
const UserInfoClasses = spacepack.findByCode(
+
"infoScroller",
+
"userInfoSection",
+
"userInfoSectionHeader"
+
)[0].exports;
+
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
+
+
function InfoSection({
+
title,
+
children
+
}: {
+
title: string;
+
children: React.ReactNode;
+
}) {
+
return (
+
<div
+
style={{
+
marginRight: "1em"
+
}}
+
>
+
<CommonComponents.Text
+
variant="eyebrow"
+
className={UserInfoClasses.userInfoSectionHeader}
+
>
+
{title}
+
</CommonComponents.Text>
+
+
<CommonComponents.Text variant="text-sm/normal">
+
{children}
+
</CommonComponents.Text>
+
</div>
+
);
+
}
+
+
function Badge({
+
color,
+
children
+
}: {
+
color: string;
+
children: React.ReactNode;
+
}) {
+
return (
+
<span
+
style={{
+
borderRadius: ".1875rem",
+
padding: "0 0.275rem",
+
marginRight: "0.4em",
+
backgroundColor: color,
+
color: "#fff"
+
}}
+
>
+
{children}
+
</span>
+
);
+
}
+
+
export default function ExtensionInfo({ ext }: { ext: MoonbaseExtension }) {
+
const authors = ext.manifest?.meta?.authors;
+
const tags = ext.manifest?.meta?.tags;
+
const version = ext.manifest?.version;
+
+
const dependencies: Dependency[] = [];
+
if (ext.manifest.dependencies != null) {
+
dependencies.push(
+
...ext.manifest.dependencies.map((dep) => ({
+
id: dep,
+
type: DependencyType.Dependency
+
}))
+
);
+
}
+
+
if (ext.manifest.suggested != null) {
+
dependencies.push(
+
...ext.manifest.suggested.map((dep) => ({
+
id: dep,
+
type: DependencyType.Optional
+
}))
+
);
+
}
+
+
if (ext.manifest.incompatible != null) {
+
dependencies.push(
+
...ext.manifest.incompatible.map((dep) => ({
+
id: dep,
+
type: DependencyType.Incompatible
+
}))
+
);
+
}
+
+
return (
+
<>
+
{authors != null && (
+
<InfoSection title="Authors">
+
{authors.map((author, i) => {
+
const comma = i !== authors.length - 1 ? ", " : "";
+
if (typeof author === "string") {
+
return (
+
<span key={i}>
+
{author}
+
{comma}
+
</span>
+
);
+
} else {
+
// TODO: resolve IDs
+
return (
+
<span key={i}>
+
{author.name}
+
{comma}
+
</span>
+
);
+
}
+
})}
+
</InfoSection>
+
)}
+
+
{tags != null && (
+
<InfoSection title="Tags">
+
{tags.map((tag, i) => {
+
const name = tagNames[tag];
+
+
return (
+
<Badge
+
key={i}
+
color={
+
tag === ExtensionTag.DangerZone
+
? "var(--red-400)"
+
: "var(--brand-500)"
+
}
+
>
+
{name}
+
</Badge>
+
);
+
})}
+
</InfoSection>
+
)}
+
+
{dependencies.length > 0 && (
+
<InfoSection title="Dependencies">
+
{dependencies.map((dep) => {
+
const colors = {
+
[DependencyType.Dependency]: "var(--brand-500)",
+
[DependencyType.Optional]: "var(--orange-400)",
+
[DependencyType.Incompatible]: "var(--red-400)"
+
};
+
const color = colors[dep.type];
+
const id = MoonbaseSettingsStore.getExtensionUniqueId(dep.id);
+
const name =
+
(id !== null
+
? MoonbaseSettingsStore.getExtensionName(id!)
+
: null) ?? dep.id;
+
return (
+
<Badge color={color} key={dep.id}>
+
{name}
+
</Badge>
+
);
+
})}
+
</InfoSection>
+
)}
+
+
{version != null && (
+
<InfoSection title="Version">
+
<span>{version}</span>
+
</InfoSection>
+
)}
+
</>
+
);
+
}
+427
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/settings.tsx
···
+
import {
+
ExtensionSettingType,
+
ExtensionSettingsManifest,
+
MultiSelectSettingType,
+
NumberSettingType,
+
SelectOption,
+
SelectSettingType
+
} from "@moonlight-mod/types/config";
+
+
import {
+
CircleXIconSVG,
+
ExtensionState,
+
MoonbaseExtension
+
} from "../../../types";
+
+
import React from "@moonlight-mod/wp/common_react";
+
import CommonComponents from "@moonlight-mod/wp/common_components";
+
import * as Flux from "@moonlight-mod/wp/common_flux";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
+
type SettingsProps = {
+
ext: MoonbaseExtension;
+
name: string;
+
setting: ExtensionSettingsManifest;
+
disabled: boolean;
+
};
+
+
type SettingsComponent = React.ComponentType<SettingsProps>;
+
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
+
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
+
+
function useConfigEntry<T>(uniqueId: number, name: string) {
+
return Flux.useStateFromStores(
+
[MoonbaseSettingsStore],
+
() => {
+
return {
+
value: MoonbaseSettingsStore.getExtensionConfig<T>(uniqueId, name),
+
displayName: MoonbaseSettingsStore.getExtensionConfigName(
+
uniqueId,
+
name
+
),
+
description: MoonbaseSettingsStore.getExtensionConfigDescription(
+
uniqueId,
+
name
+
)
+
};
+
},
+
[uniqueId, name]
+
);
+
}
+
+
function Boolean({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormSwitch } = CommonComponents;
+
const { value, displayName, description } = useConfigEntry<boolean>(
+
ext.uniqueId,
+
name
+
);
+
+
return (
+
<FormSwitch
+
value={value ?? false}
+
hideBorder={true}
+
disabled={disabled}
+
onChange={(value: boolean) => {
+
MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value);
+
}}
+
note={description}
+
className={`${Margins.marginReset} ${Margins.marginTop20}`}
+
>
+
{displayName}
+
</FormSwitch>
+
);
+
}
+
+
function Number({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormItem, FormText, Slider } = CommonComponents;
+
const { value, displayName, description } = useConfigEntry<number>(
+
ext.uniqueId,
+
name
+
);
+
+
const castedSetting = setting as NumberSettingType;
+
const min = castedSetting.min ?? 0;
+
const max = castedSetting.max ?? 100;
+
+
return (
+
<FormItem className={Margins.marginTop20} title={displayName}>
+
{description && <FormText>{description}</FormText>}
+
<Slider
+
initialValue={value ?? 0}
+
disabled={disabled}
+
minValue={castedSetting.min ?? 0}
+
maxValue={castedSetting.max ?? 100}
+
onValueChange={(value: number) => {
+
const rounded = Math.max(min, Math.min(max, Math.round(value)));
+
MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, rounded);
+
}}
+
/>
+
</FormItem>
+
);
+
}
+
+
function String({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormItem, FormText, TextInput } = CommonComponents;
+
const { value, displayName, description } = useConfigEntry<string>(
+
ext.uniqueId,
+
name
+
);
+
+
return (
+
<FormItem className={Margins.marginTop20} title={displayName}>
+
{description && (
+
<FormText className={Margins.marginBottom8}>{description}</FormText>
+
)}
+
<TextInput
+
value={value ?? ""}
+
onChange={(value: string) => {
+
if (disabled) return;
+
MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value);
+
}}
+
/>
+
</FormItem>
+
);
+
}
+
+
function MultilineString({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormItem, FormText, TextArea } = CommonComponents;
+
const { value, displayName, description } = useConfigEntry<string>(
+
ext.uniqueId,
+
name
+
);
+
+
return (
+
<FormItem className={Margins.marginTop20} title={displayName}>
+
{description && (
+
<FormText className={Margins.marginBottom8}>{description}</FormText>
+
)}
+
<TextArea
+
rows={5}
+
value={value ?? ""}
+
className={"moonbase-resizeable"}
+
onChange={(value: string) => {
+
if (disabled) return;
+
MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value);
+
}}
+
/>
+
</FormItem>
+
);
+
}
+
+
function Select({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormItem, FormText, SingleSelect } = CommonComponents;
+
const { value, displayName, description } = useConfigEntry<string>(
+
ext.uniqueId,
+
name
+
);
+
+
const castedSetting = setting as SelectSettingType;
+
const options = castedSetting.options;
+
+
return (
+
<FormItem className={Margins.marginTop20} title={displayName}>
+
{description && (
+
<FormText className={Margins.marginBottom8}>{description}</FormText>
+
)}
+
<SingleSelect
+
autofocus={false}
+
clearable={false}
+
value={value ?? ""}
+
options={options.map((o: SelectOption) =>
+
typeof o === "string" ? { value: o, label: o } : o
+
)}
+
onChange={(value: string) => {
+
if (disabled) return;
+
MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value);
+
}}
+
/>
+
</FormItem>
+
);
+
}
+
+
function MultiSelect({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormItem, FormText, Select, useVariableSelect, multiSelect } =
+
CommonComponents;
+
const { value, displayName, description } = useConfigEntry<string | string[]>(
+
ext.uniqueId,
+
name
+
);
+
+
const castedSetting = setting as MultiSelectSettingType;
+
const options = castedSetting.options;
+
+
return (
+
<FormItem className={Margins.marginTop20} title={displayName}>
+
{description && (
+
<FormText className={Margins.marginBottom8}>{description}</FormText>
+
)}
+
<Select
+
autofocus={false}
+
clearable={false}
+
closeOnSelect={false}
+
options={options.map((o: SelectOption) =>
+
typeof o === "string" ? { value: o, label: o } : o
+
)}
+
{...useVariableSelect({
+
onSelectInteraction: multiSelect,
+
value: new Set(Array.isArray(value) ? value : [value]),
+
onChange: (value: string) => {
+
if (disabled) return;
+
MoonbaseSettingsStore.setExtensionConfig(
+
ext.uniqueId,
+
name,
+
Array.from(value)
+
);
+
}
+
})}
+
/>
+
</FormItem>
+
);
+
}
+
+
const RemoveButtonClasses = spacepack.findByCode("removeButtonContainer")[0]
+
.exports;
+
const CircleXIcon = spacepack.findByCode(CircleXIconSVG)[0].exports.default;
+
function RemoveEntryButton({
+
onClick,
+
disabled
+
}: {
+
onClick: () => void;
+
disabled: boolean;
+
}) {
+
const { Tooltip, Clickable } = CommonComponents;
+
return (
+
<div className={RemoveButtonClasses.removeButtonContainer}>
+
<Tooltip text="Remove entry" position="top">
+
{(props: any) => (
+
<Clickable
+
{...props}
+
className={RemoveButtonClasses.removeButton}
+
onClick={onClick}
+
>
+
<CircleXIcon width={16} height={16} />
+
</Clickable>
+
)}
+
</Tooltip>
+
</div>
+
);
+
}
+
+
function List({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormItem, FormText, TextInput, Button, Flex } = CommonComponents;
+
const { value, displayName, description } = useConfigEntry<string[]>(
+
ext.uniqueId,
+
name
+
);
+
+
const entries = value ?? [];
+
const updateConfig = () =>
+
MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, entries);
+
+
return (
+
<FormItem className={Margins.marginTop20} title={displayName}>
+
{description && (
+
<FormText className={Margins.marginBottom4}>{description}</FormText>
+
)}
+
<Flex direction={Flex.Direction.VERTICAL}>
+
{entries.map((val, i) => (
+
// FIXME: stylesheets
+
<div
+
key={i}
+
style={{
+
display: "grid",
+
height: "32px",
+
gap: "8px",
+
gridTemplateColumns: "1fr 32px",
+
alignItems: "center"
+
}}
+
>
+
<TextInput
+
size={TextInput.Sizes.MINI}
+
value={val}
+
disabled={disabled}
+
onChange={(newVal: string) => {
+
entries[i] = newVal;
+
updateConfig();
+
}}
+
/>
+
<RemoveEntryButton
+
disabled={disabled}
+
onClick={() => {
+
entries.splice(i, 1);
+
updateConfig();
+
}}
+
/>
+
</div>
+
))}
+
+
<Button
+
look={Button.Looks.FILLED}
+
color={Button.Colors.GREEN}
+
size={Button.Sizes.SMALL}
+
disabled={disabled}
+
className={Margins.marginTop8}
+
onClick={() => {
+
entries.push("");
+
updateConfig();
+
}}
+
>
+
Add new entry
+
</Button>
+
</Flex>
+
</FormItem>
+
);
+
}
+
+
function Dictionary({ ext, name, setting, disabled }: SettingsProps) {
+
const { FormItem, FormText, TextInput, Button, Flex } = CommonComponents;
+
const { value, displayName, description } = useConfigEntry<
+
Record<string, string>
+
>(ext.uniqueId, name);
+
+
const entries = Object.entries(value ?? {});
+
const updateConfig = () =>
+
MoonbaseSettingsStore.setExtensionConfig(
+
ext.uniqueId,
+
name,
+
Object.fromEntries(entries)
+
);
+
+
return (
+
<FormItem className={Margins.marginTop20} title={displayName}>
+
{description && (
+
<FormText className={Margins.marginBottom4}>{description}</FormText>
+
)}
+
<Flex direction={Flex.Direction.VERTICAL}>
+
{entries.map(([key, val], i) => (
+
// FIXME: stylesheets
+
<div
+
key={i}
+
style={{
+
display: "grid",
+
height: "32px",
+
gap: "8px",
+
gridTemplateColumns: "1fr 1fr 32px",
+
alignItems: "center"
+
}}
+
>
+
<TextInput
+
size={TextInput.Sizes.MINI}
+
value={key}
+
disabled={disabled}
+
onChange={(newKey: string) => {
+
entries[i][0] = newKey;
+
updateConfig();
+
}}
+
/>
+
<TextInput
+
size={TextInput.Sizes.MINI}
+
value={val}
+
disabled={disabled}
+
onChange={(newValue: string) => {
+
entries[i][1] = newValue;
+
updateConfig();
+
}}
+
/>
+
<RemoveEntryButton
+
disabled={disabled}
+
onClick={() => {
+
entries.splice(i, 1);
+
updateConfig();
+
}}
+
/>
+
</div>
+
))}
+
+
<Button
+
look={Button.Looks.FILLED}
+
color={Button.Colors.GREEN}
+
size={Button.Sizes.SMALL}
+
className={Margins.marginTop8}
+
disabled={disabled}
+
onClick={() => {
+
entries.push([`entry-${entries.length}`, ""]);
+
updateConfig();
+
}}
+
>
+
Add new entry
+
</Button>
+
</Flex>
+
</FormItem>
+
);
+
}
+
+
function Setting({ ext, name, setting, disabled }: SettingsProps) {
+
const elements: Partial<Record<ExtensionSettingType, SettingsComponent>> = {
+
[ExtensionSettingType.Boolean]: Boolean,
+
[ExtensionSettingType.Number]: Number,
+
[ExtensionSettingType.String]: String,
+
[ExtensionSettingType.MultilineString]: MultilineString,
+
[ExtensionSettingType.Select]: Select,
+
[ExtensionSettingType.MultiSelect]: MultiSelect,
+
[ExtensionSettingType.List]: List,
+
[ExtensionSettingType.Dictionary]: Dictionary
+
};
+
const element = elements[setting.type];
+
if (element == null) return <></>;
+
return React.createElement(element, { ext, name, setting, disabled });
+
}
+
+
export default function Settings({ ext }: { ext: MoonbaseExtension }) {
+
const { Flex } = CommonComponents;
+
return (
+
<Flex className="moonbase-settings" direction={Flex.Direction.VERTICAL}>
+
{Object.entries(ext.manifest.settings!).map(([name, setting]) => (
+
<Setting
+
ext={ext}
+
key={name}
+
name={name}
+
setting={setting}
+
disabled={ext.state === ExtensionState.NotDownloaded}
+
/>
+
))}
+
</Flex>
+
);
+
}
+64
packages/core-extensions/src/moonbase/webpackModules/ui/index.tsx
···
+
import React from "@moonlight-mod/wp/common_react";
+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
+
import { Text, TabBar } from "@moonlight-mod/wp/common_components";
+
+
import ExtensionsPage from "./extensions";
+
import ConfigPage from "./config";
+
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
+
+
const { Divider } = spacepack.findByCode(".default.HEADER_BAR")[0].exports
+
.default;
+
const TitleBarClasses = spacepack.findByCode("iconWrapper:", "children:")[0]
+
.exports;
+
const TabBarClasses = spacepack.findByCode("nowPlayingColumn:")[0].exports;
+
+
export const pages: Record<
+
string,
+
{
+
name: string;
+
element: React.FunctionComponent;
+
}
+
> = {
+
extensions: {
+
name: "Extensions",
+
element: ExtensionsPage
+
},
+
config: {
+
name: "Config",
+
element: ConfigPage
+
}
+
};
+
+
export function Moonbase() {
+
const [selectedTab, setSelectedTab] = React.useState(Object.keys(pages)[0]);
+
+
return (
+
<>
+
<div className={`${TitleBarClasses.children} ${Margins.marginBottom20}`}>
+
<Text
+
className={TitleBarClasses.titleWrapper}
+
variant="heading-lg/semibold"
+
tag="h2"
+
>
+
Moonbase
+
</Text>
+
<Divider />
+
<TabBar
+
selectedItem={selectedTab}
+
onItemSelect={setSelectedTab}
+
type="top-pill"
+
className={TabBarClasses.tabBar}
+
>
+
{Object.entries(pages).map(([id, page]) => (
+
<TabBar.Item key={id} id={id} className={TabBarClasses.item}>
+
{page.name}
+
</TabBar.Item>
+
))}
+
</TabBar>
+
</div>
+
+
{React.createElement(pages[selectedTab].element)}
+
</>
+
);
+
}
+7
packages/core-extensions/src/moonbase/wp.d.ts
···
+
declare module "@moonlight-mod/wp/moonbase_ui" {
+
export * from "core-extensions/src/moonbase/webpackModules/ui";
+
}
+
+
declare module "@moonlight-mod/wp/moonbase_stores" {
+
export * from "core-extensions/src/moonbase/webpackModules/stores";
+
}
+3 -6
packages/core-extensions/src/quietLoggers/index.ts
···
"is not a valid locale",
/(.)\.error\(""\.concat\((.)," is not a valid locale\."\)\)/g
],
-
['.displayName="RunningGameStore"', /.\.info\("games",{.+?}\),/],
+
['="RunningGameStore"', /.\.info\("games",{.+?}\),/],
[
'"[BUILD INFO] Release Channel: "',
-
/new\(0,.{1,2}\.default\)\(\)\.log\("\[BUILD INFO\] Release Channel: ".+?"\)\),/
+
/new .{1,2}\.default\(\)\.log\("\[BUILD INFO\] Release Channel: ".+?"\)\),/
],
[
'.AnalyticEvents.APP_NATIVE_CRASH,"Storage"',
···
'.AnalyticEvents.APP_NATIVE_CRASH,"Storage"',
'console.log("AppCrashedFatalReport: getLastCrash not supported.");'
],
-
[
-
'"[NATIVE INFO] ',
-
/new\(0,.{1,2}\.default\)\(\)\.log\("\[NATIVE INFO] .+?\)\),/
-
],
+
['"[NATIVE INFO] ', /new .{1,2}\.default\(\)\.log\("\[NATIVE INFO] .+?\)\),/],
['"Spellchecker"', /.\.info\("Switching to ".+?"\(unavailable\)"\);?/g],
[
'throw new Error("Messages are still loading.");',
+3 -3
packages/core-extensions/src/settings/index.ts
···
{
find: ".UserSettingsSections.HOTSPOT_OPTIONS",
replace: {
-
match: /\.CUSTOM,element:(.+?)}\];return (.{1,2})/,
-
replacement: (_, lastElement, sections) =>
-
`.CUSTOM,element:${lastElement}}];return require("settings_settings").Settings._mutateSections(${sections})`
+
match: /return(\[.+?\.CUSTOM,element:.+?}\])}/,
+
replacement: (_, arr) =>
+
`return require("settings_settings").Settings._mutateSections(${arr})}`
}
},
{
+19 -8
packages/core-extensions/src/spacepack/webpackModules/spacepack.ts
···
);
},
-
lazyLoad: (find: string | RegExp | (string | RegExp)[], match: RegExp) => {
-
const module = Array.isArray(find)
+
lazyLoad: (
+
find: string | RegExp | (string | RegExp)[],
+
chunk: RegExp,
+
module: RegExp
+
) => {
+
if (!chunk.flags.includes("g"))
+
return Promise.reject("Chunk ID regex must be global");
+
+
const mod = Array.isArray(find)
? spacepack.findByCode(...find)
: spacepack.findByCode(find);
-
if (module.length < 1) return Promise.reject("Find failed");
+
if (mod.length < 1) return Promise.reject("Module find failed");
-
const findId = module[0].id;
+
const findId = mod[0].id;
const findCode = webpackRequire.m[findId].toString().replace(/\n/g, "");
-
const matchResult = findCode.match(match);
-
if (!matchResult) return Promise.reject("Match failed");
+
const chunkIds = [...findCode.matchAll(chunk)].map(([, id]) => id);
+
if (chunkIds.length === 0) return Promise.reject("Chunk ID match failed");
+
+
const moduleId = findCode.match(module)?.[1];
+
if (!moduleId) return Promise.reject("Module ID match failed");
-
const matchId = matchResult[1];
-
return webpackRequire.el(matchId).then(() => webpackRequire(matchId));
+
return Promise.all(chunkIds.map((c) => webpackRequire.e(c))).then(() =>
+
webpackRequire(moduleId)
+
);
},
filterReal: (modules: WebpackModule[]) => {
+8 -6
packages/core/src/extension.ts
···
const path = requireImport("path");
const ret = [];
-
for (const file of fs.readdirSync(dir)) {
-
if (file === "manifest.json") {
-
ret.push(path.join(dir, file));
-
}
+
if (fs.existsSync(dir)) {
+
for (const file of fs.readdirSync(dir)) {
+
if (file === "manifest.json") {
+
ret.push(path.join(dir, file));
+
}
-
if (fs.statSync(path.join(dir, file)).isDirectory()) {
-
ret.push(...findManifests(path.join(dir, file)));
+
if (fs.statSync(path.join(dir, file)).isDirectory()) {
+
ret.push(...findManifests(path.join(dir, file)));
+
}
}
}
+11 -7
packages/core/src/patch.ts
···
export function registerPatch(patch: IdentifiedPatch) {
patches.push(patch);
+
moonlight.unpatched.add(patch);
}
export function registerWebpackModule(wp: IdentifiedWebpackModule) {
webpackModules.add(wp);
+
if (wp.dependencies?.length) {
+
moonlight.pendingModules.add(wp);
+
}
}
/*
···
for (const [_modId, mod] of Object.entries(entry)) {
const modStr = mod.toString();
-
const wpModules = Array.from(webpackModules.values());
-
for (const wpModule of wpModules) {
+
for (const wpModule of webpackModules) {
const id = wpModule.ext + "_" + wpModule.id;
if (wpModule.dependencies) {
const deps = new Set(wpModule.dependencies);
···
// FIXME: This dependency resolution might fail if the things we want
// got injected earlier. If weird dependencies fail, this is likely why.
if (deps.size) {
-
for (const dep of deps.values()) {
+
for (const dep of deps) {
if (typeof dep === "string") {
if (modStr.includes(dep)) deps.delete(dep);
} else if (dep instanceof RegExp) {
···
}
if (deps.size !== 0) {
-
// Update the deps that have passed
-
webpackModules.delete(wpModule);
wpModule.dependencies = Array.from(deps);
-
webpackModules.add(wpModule);
continue;
}
···
}
webpackModules.delete(wpModule);
+
moonlight.pendingModules.delete(wpModule);
injectedWpModules.push(wpModule);
inject = true;
-
if (wpModule.run) modules[id] = wpModule.run;
+
if (wpModule.run) {
+
modules[id] = wpModule.run;
+
wpModule.run.__moonlight = true;
+
}
if (wpModule.entrypoint) entrypoints.push(id);
}
if (!webpackModules.size) break;
+1 -2
packages/types/package.json
···
{
"name": "@moonlight-mod/types",
-
"version": "1.1.6",
+
"version": "1.1.7",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
···
},
"dependencies": {
"@types/flux": "^3.1.12",
-
"@types/node": "^20.6.2",
"@types/react": "^18.2.22",
"csstype": "^3.1.2",
"standalone-electron-types": "^1.0.0"
+7
packages/types/src/config.ts
···
Boolean = "boolean",
Number = "number",
String = "string",
+
MultilineString = "multilinestring",
Select = "select",
MultiSelect = "multiselect",
List = "list",
···
export type StringSettingType = {
type: ExtensionSettingType.String;
+
default?: string;
+
};
+
+
export type MultilineTextInputSettingType = {
+
type: ExtensionSettingType.MultilineString;
default?: string;
};
···
| BooleanSettingType
| NumberSettingType
| StringSettingType
+
| MultilineTextInputSettingType
| SelectSettingType
| MultiSelectSettingType
| ListSettingType
+3 -1
packages/types/src/coreExtensions.ts
···
) => Function | null;
lazyLoad: (
find: string | RegExp | (string | RegExp)[],
-
match: RegExp
+
chunk: RegExp,
+
module: RegExp
) => Promise<any>;
filterReal: (modules: WebpackModule[]) => WebpackModule[];
};
···
export type CommonFluxDispatcher = Dispatcher<any>;
export * as Markdown from "./coreExtensions/markdown";
+
export * as ContextMenu from "./coreExtensions/contextMenu";
+45
packages/types/src/coreExtensions/components.ts
···
Sizes: typeof TextInputSizes;
}
+
export enum TextAreaAutoComplete {
+
ON = "on",
+
OFF = "off"
+
}
+
+
export enum TextAreaWrap {
+
HARD = "hard",
+
SOFT = "soft",
+
OFF = "off"
+
}
+
+
interface TextArea
+
extends ComponentClass<
+
PropsWithoutRef<{
+
value?: string;
+
defaultValue?: string;
+
autoComplete?: TextAreaAutoComplete;
+
autoFocus?: boolean;
+
cols?: number;
+
disabled?: boolean;
+
form?: string;
+
maxLength?: number;
+
minLength?: number;
+
name?: string;
+
onChange?: (value: string, name: string) => void;
+
onChangeCapture?: (value: string, name: string) => void;
+
onInput?: (value: string, name: string) => void;
+
onInputCapture?: (value: string, name: string) => void;
+
onInvalid?: (value: string, name: string) => void;
+
onInvalidCapture?: (value: string, name: string) => void;
+
onSelect?: (value: string, name: string) => void;
+
onSelectCapture?: (value: string, name: string) => void;
+
placeholder?: string;
+
readOnly?: boolean;
+
required?: boolean;
+
rows?: number;
+
wrap?: TextAreaWrap;
+
className?: string;
+
}>
+
> {
+
AutoCompletes: typeof TextAreaAutoComplete;
+
Wraps: typeof TextAreaWrap;
+
}
+
export enum FormTextTypes {
DEFAULT = "default",
DESCRIPTION = "description",
···
}>
>;
TextInput: TextInput;
+
TextArea: TextArea;
FormDivider: ComponentClass<any>;
FormSection: ComponentClass<
PropsWithChildren<{
+154
packages/types/src/coreExtensions/contextMenu.ts
···
+
// TODO: Deduplicate common props
+
+
export type Menu = React.FunctionComponent<{
+
navId: string;
+
variant?: string;
+
hideScrollbar?: boolean;
+
className?: string;
+
children: React.ReactComponentElement<MenuElement>[];
+
onClose?: () => void;
+
onSelect?: () => void;
+
}>;
+
export type MenuProps = React.ComponentProps<Menu>;
+
+
export type MenuElement =
+
| MenuSeparator
+
| MenuGroup
+
| MenuItem
+
| MenuCheckboxItem
+
| MenuRadioItem
+
| MenuControlItem;
+
+
/* eslint-disable prettier/prettier */
+
export type MenuSeparator = React.FunctionComponent;
+
export type MenuGroup = React.FunctionComponent<{
+
label?: string;
+
className?: string;
+
color?: string;
+
children: React.ReactComponentElement<MenuElement>[];
+
}>;
+
export type MenuItem = React.FunctionComponent<{
+
id: any;
+
dontCloseOnActionIfHoldingShiftKey?: boolean;
+
} & ({
+
label: string;
+
subtext?: string;
+
color?: string;
+
hint?: string;
+
disabled?: boolean;
+
icon?: any;
+
showIconFirst?: boolean;
+
imageUrl?: string;
+
+
className?: string;
+
focusedClassName?: string;
+
subMenuIconClassName?: string;
+
+
action?: () => void;
+
onFocus?: () => void;
+
+
iconProps?: any;
+
sparkle?: any;
+
+
children?: React.ReactComponentElement<MenuElement>[];
+
onChildrenScroll?: any;
+
childRowHeight?: any;
+
listClassName?: string;
+
subMenuClassName?: string;
+
} | {
+
color?: string;
+
disabled?: boolean;
+
keepItemStyles?: boolean;
+
+
action?: () => void;
+
+
render: any;
+
navigable?: boolean;
+
})>;
+
export type MenuCheckboxItem = React.FunctionComponent<{
+
id: any;
+
label: string;
+
subtext?: string;
+
color?: string;
+
className?: string;
+
focusedClassName?: string;
+
disabled?: boolean;
+
checked: boolean;
+
action?: () => void;
+
}>;
+
export type MenuRadioItem = React.FunctionComponent<{
+
id: any;
+
label: string;
+
subtext?: string;
+
color?: string;
+
disabled?: boolean;
+
action?: () => void;
+
}>;
+
export type MenuControlItem = React.FunctionComponent<{
+
id: any;
+
label: string;
+
color?: string;
+
disabled?: boolean;
+
showDefaultFocus?: boolean;
+
} & ({
+
control: any;
+
} | {
+
control?: undefined;
+
interactive?: boolean;
+
children?: React.ReactComponentElement<MenuElement>[];
+
})>;
+
/* eslint-disable prettier/prettier */
+
+
export type ContextMenu = {
+
addItem: (
+
navId: string,
+
item: (props: any) => React.ReactComponentElement<MenuElement>,
+
anchorId: string,
+
before?: boolean
+
) => void;
+
+
MenuCheckboxItem: MenuCheckboxItem;
+
MenuControlItem: MenuControlItem;
+
MenuGroup: MenuGroup;
+
MenuItem: MenuItem;
+
MenuRadioItem: MenuRadioItem;
+
MenuSeparator: MenuSeparator;
+
};
+
+
export type InternalItem = {
+
type: string;
+
key?: string;
+
};
+
+
export type InternalSeparator = {
+
type: "separator";
+
navigable: false;
+
};
+
export type InternalGroupStart = {
+
type: "groupstart";
+
length: number;
+
navigable: false;
+
props: React.ComponentProps<MenuGroup>;
+
};
+
export type InternalGroupEnd = {
+
type: "groupend";
+
} & Omit<InternalGroupStart, "type">;
+
export type InternalCustomItem = {
+
type: "customitem";
+
key: any;
+
navigable?: boolean;
+
render: any;
+
props: Extract<React.ComponentProps<MenuItem>, { render: any }>;
+
};
+
export type InternalItem_ = {
+
type: "item";
+
key: any;
+
navigable: true;
+
label: string;
+
};
+
+
export type EvilItemParser = (
+
el:
+
| React.ReactComponentElement<MenuElement>
+
| React.ReactComponentElement<MenuElement>[]
+
) => InternalItem[];
+4
packages/types/src/discord/require.ts
···
CommonComponents,
CommonFluxDispatcher
} from "../coreExtensions";
+
import { ContextMenu, EvilItemParser } from "../coreExtensions/contextMenu";
import { Markdown } from "../coreExtensions/markdown";
declare function WebpackRequire(id: string): any;
···
};
declare function WebpackRequire(id: "markdown_markdown"): Markdown;
+
+
declare function WebpackRequire(id: "contextMenu_evilMenu"): EvilItemParser;
+
declare function WebpackRequire(id: "contextMenu_contextMenu"): ContextMenu;
export default WebpackRequire;
+1 -1
packages/types/src/discord/webpack.ts
···
export type WebpackRequireType = typeof WebpackRequire & {
c: Record<string, WebpackModule>;
m: Record<string, WebpackModuleFunc>;
-
el: (module: number | string) => Promise<void>;
+
e: (module: number | string) => Promise<void>;
};
export type WebpackModule = {
+2
packages/types/src/globals.ts
···
import {
DetectedExtension,
IdentifiedPatch,
+
IdentifiedWebpackModule,
ProcessedExtensions
} from "./extension";
import EventEmitter from "events";
···
export type MoonlightWeb = {
unpatched: Set<IdentifiedPatch>;
+
pendingModules: Set<IdentifiedWebpackModule>;
enabledExtensions: Set<string>;
getConfig: (ext: string) => ConfigExtension["config"];
+18 -5
packages/types/src/import.d.ts
···
declare module "@moonlight-mod/wp/common_components" {
import { CoreExtensions } from "@moonlight-mod/types";
-
const components: CoreExtensions.CommonComponents;
-
export default components;
-
export = components;
+
const CommonComponent: CoreExtensions.CommonComponents;
+
export = CommonComponent;
}
declare module "@moonlight-mod/wp/common_flux" {
import { CoreExtensions } from "@moonlight-mod/types";
const Flux: CoreExtensions.CommonFlux;
-
export default Flux;
+
// FIXME: This is wrong, the default export differs from the named exports.
+
export = Flux;
}
declare module "@moonlight-mod/wp/common_fluxDispatcher" {
···
export default Dispatcher;
}
+
declare module "@moonlight-mod/wp/common_stores";
+
declare module "@moonlight-mod/wp/common_react" {
import React from "react";
export = React;
···
import { CoreExtensions } from "@moonlight-mod/types";
export const Settings: CoreExtensions.Settings;
export default Settings;
-
export = Settings;
}
declare module "@moonlight-mod/wp/markdown_markdown" {
···
const Markdown: CoreExtensions.Markdown.Markdown;
export = Markdown;
}
+
+
declare module "@moonlight-mod/wp/contextMenu_evilMenu" {
+
import { CoreExtensions } from "@moonlight-mod/types";
+
const EvilParser: CoreExtensions.ContextMenu.EvilItemParser;
+
export = EvilParser;
+
}
+
+
declare module "@moonlight-mod/wp/contextMenu_contextMenu" {
+
import { CoreExtensions } from "@moonlight-mod/types";
+
const ContextMenu: CoreExtensions.ContextMenu.ContextMenu;
+
export = ContextMenu;
+
}
-1
packages/types/src/index.ts
···
-
/// <reference types="node" />
/// <reference types="standalone-electron-types" />
/// <reference types="react" />
/// <reference types="flux" />
-1
packages/types/tsconfig.json
···
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
-
"skipLibCheck": true,
"moduleResolution": "bundler",
"jsx": "react",
"declaration": true
+1
packages/web-preload/src/index.ts
···
window.moonlight = {
unpatched: new Set(),
+
pendingModules: new Set(),
enabledExtensions: new Set(),
getConfig: moonlightNode.getConfig.bind(moonlightNode),
-7
pnpm-lock.yaml
···
'@types/flux':
specifier: ^3.1.12
version: 3.1.12
-
'@types/node':
-
specifier: ^20.6.2
-
version: 20.6.2
'@types/react':
specifier: ^18.2.22
version: 18.2.22
···
/@types/node@18.17.17:
resolution: {integrity: sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==}
-
dev: false
-
-
/@types/node@20.6.2:
-
resolution: {integrity: sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==}
dev: false
/@types/prop-types@15.7.6:
-1
tsconfig.json
···
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
-
"skipLibCheck": true,
"moduleResolution": "bundler",
"baseUrl": "./packages/",
"jsx": "react",