mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dc72a76ba | |||
| 58172606f3 | |||
| 6e07adb990 | |||
| 4abf0e9ec4 | |||
| 269e6de18b | |||
| fdf8351adf | |||
| fbf2fff618 | |||
| 0dfdba702d | |||
| 4f41cd2cf3 | |||
| 6d2e3412d4 | |||
| fa200abb1d | |||
| 513aef87fc | |||
| 9fbceb1fe4 | |||
| 594a71ee36 | |||
| 7242c233f6 | |||
| 47314ffbfa | |||
| 7f37433819 | |||
| 69ea65f758 | |||
| 11b89eab9f | |||
| dd108d73e2 | |||
| 3d73ace19d | |||
| f1c879f811 | |||
| 47e1c58acf | |||
| 911ef41d60 | |||
| 93fc420974 | |||
| 6076483b38 | |||
| dc0d5661b6 | |||
| 55e94c9e5d | |||
| d6b7480283 | |||
| f7bd0f85f6 | |||
| bd40bf6519 | |||
| c628d88957 | |||
| 6d0a205f5a | |||
| 5c478979e9 | |||
| 417e538df6 | |||
| 573da853d7 | |||
| 3c7731be19 | |||
| 51487406fe | |||
| cf53ab43c8 | |||
| 7952615920 | |||
| 8c2292eae7 | |||
| 2207eca28d | |||
| 5e12baa35f | |||
| 7b28255ffa | |||
| df20e0dfb5 | |||
| 840ff225f8 | |||
| 4e7865e312 | |||
| 4702f67767 | |||
| 9d2ec1166e | |||
| 337697fafb | |||
| a43e238905 | |||
| d253d7aad0 | |||
| e75cd44872 | |||
| e76912004a | |||
| af95b24005 | |||
| 21d61ecaa7 | |||
| 98dccb1831 | |||
| e8cd849623 | |||
| 45ceae3b07 | |||
| cd11f8a39f | |||
| e9c5c21698 | |||
| 90de63c35f | |||
| fbb2eca661 | |||
| 7c2309c620 | |||
| 3b854d94a6 | |||
| ad51f88358 | |||
| 347e84247d | |||
| acb9990dfb | |||
| d5a0a295eb | |||
| c619d4a608 | |||
| a16fcacd69 | |||
| 93e4d22623 | |||
| 93dd11d5a2 | |||
| 0702c0db18 | |||
| d491b24149 |
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: Zagrios
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[AME]"
|
||||
labels: enhancement
|
||||
assignees: Zagrios
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,3 @@
|
||||
## [0.1.0](https://github.com/Zagrios/bs-manager/releases/tag/v0.1.0) (September 19, 2022)
|
||||
|
||||
* Initial public release
|
||||
@@ -1,21 +1,339 @@
|
||||
The MIT License (MIT)
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (c) 2015-present Electron React Boilerplate
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
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
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE 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.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
@@ -66,7 +66,7 @@ Otherwise you can add on to existing issues if one matches what you're encounter
|
||||
</p>
|
||||
<h2 id="acknowledgements">Acknowledgement</h2>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Zagrios">Zagrios</a> (Mathieu GRIES) - Founder, Lead Developer & Project Lead</li>
|
||||
<li><a href="https://github.com/Zagrios">Zagrios</a> - Founder, Lead Developer & Project Lead</li>
|
||||
<li><a href="https://github.com/Iluhadesu">Iluhadesu</a> - Custom DepotDownloader Developer</li>
|
||||
<li><a href="https://github.com/GaetanGrd">Gaëtan Gerard</a> - Main Alpha Tester</li>
|
||||
</ul>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
@@ -477,5 +477,21 @@
|
||||
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/82c29d674bc57596102a6b815f66e956f217e658.png",
|
||||
"ReleaseDate": "1661864214",
|
||||
"year": "2022"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.25.0",
|
||||
"BSManifest": "4529582051007915041",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/3312985013538143172",
|
||||
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/54ac649877eca25a2a6449662337e2c63c51bbcf.png",
|
||||
"ReleaseDate": "1665068529",
|
||||
"year": "2022"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.25.1",
|
||||
"BSManifest": "7372469314660649150",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/5346361068670142187",
|
||||
"ReleaseImg": "https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/clans//32055887/52cbd15b5e226f509e051565f6e4935eb0e44e2c.png",
|
||||
"ReleaseDate": "1665673269",
|
||||
"year": "2022"
|
||||
}
|
||||
]
|
||||
@@ -1 +1,3 @@
|
||||
[]
|
||||
[
|
||||
{"username": "Shidorien"}
|
||||
]
|
||||
@@ -14,6 +14,24 @@
|
||||
"desktop": "Desktop Mod",
|
||||
"debug": "Debug Mod"
|
||||
},
|
||||
"mods":{
|
||||
"loading-mods": "Loading mods...",
|
||||
"buttons":{
|
||||
"more-infos": "More info",
|
||||
"install-or-update": "Install or update"
|
||||
},
|
||||
"mods-grid":{
|
||||
"header-bar":{
|
||||
"name": "Name",
|
||||
"installed": "Installed",
|
||||
"latest": "Latest",
|
||||
"description":"Description",
|
||||
"dropdown":{
|
||||
"uninstall-all":"Uninstall all"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown":{
|
||||
"open-folder": "Open folder",
|
||||
"verify-files": "Verify files",
|
||||
@@ -70,8 +88,20 @@
|
||||
"buttons":{
|
||||
"support": "Support BSManager",
|
||||
"supporters":"Supporters 👀"
|
||||
},
|
||||
"view":{
|
||||
"no-supporters": "No supporter yet",
|
||||
"sponsors":"Sponsors",
|
||||
"supporters":"Supporters"
|
||||
}
|
||||
},
|
||||
"contribution":{
|
||||
"description":"Suggest new features or report a bug to help improve BSManager!",
|
||||
"buttons":{
|
||||
"request-features":"Request a feature",
|
||||
"report-bug":"Report a bug",
|
||||
"open-logs": "Open logs"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81,6 +111,11 @@
|
||||
"warning": "⚠️ Warning",
|
||||
"success": "🎉 Success"
|
||||
},
|
||||
"common":{
|
||||
"msg":{
|
||||
"error-occurred": "An error has occurred"
|
||||
}
|
||||
},
|
||||
"shared":{
|
||||
"errors":{
|
||||
"titles":{
|
||||
@@ -196,7 +231,44 @@
|
||||
"CloningFinished": "Cloning complete 🎉"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods":{
|
||||
"install-mods":{
|
||||
"titles":{
|
||||
"success": "Mods installed 🎉",
|
||||
"warning": "Mods installed 🤔"
|
||||
},
|
||||
"msg":{
|
||||
"success": "All mods have been installed.",
|
||||
"warning": "One or more mods could not be installed.",
|
||||
"errors":{
|
||||
"no-mods": "No mods to install.",
|
||||
"cannot-install-bsipa": "BSIPA installation failed 😨"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"titles":{
|
||||
"success": "Mod uninstalled 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"errors":{
|
||||
"no-mods": "This mod isn't installed 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"titles":{
|
||||
"success": "Mods uninstalled 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"success": "All mods have been uninstalled.",
|
||||
"errors": {
|
||||
"no-mods": "No mod is installed in this version 😑"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals":{
|
||||
"guard":{
|
||||
@@ -256,7 +328,16 @@
|
||||
"buttons":{
|
||||
"submit": "Clone"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"title": "Uninstall",
|
||||
"description": "Are you sure you want to uninstall {mod}? It could cause other installed mods to malfunction.",
|
||||
"description-bsipa": "Are you sure you want to uninstall BSIPA? After that all installed mods will not work anymore."
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"title": "Uninstalling mods",
|
||||
"description": "Are you sure you want to uninstall all the mods from version {version}? This operation cannot be undone."
|
||||
}
|
||||
},
|
||||
"auto-update":{
|
||||
"checking": "Checking for updates",
|
||||
|
||||
@@ -14,6 +14,24 @@
|
||||
"desktop": "Desktop Mod",
|
||||
"debug": "Debug Mod"
|
||||
},
|
||||
"mods":{
|
||||
"loading-mods": "Cargando mods...",
|
||||
"buttons":{
|
||||
"more-infos": "Más información",
|
||||
"install-or-update": "Instalar o actualizar"
|
||||
},
|
||||
"mods-grid":{
|
||||
"header-bar":{
|
||||
"name": "Nombre",
|
||||
"installed": "Instalado",
|
||||
"latest": "Último",
|
||||
"description":"Descripción",
|
||||
"dropdown":{
|
||||
"uninstall-all":"Desinstalar todos"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown":{
|
||||
"open-folder": "Abrir la carpeta",
|
||||
"verify-files": "Verificar los archivos",
|
||||
@@ -70,9 +88,22 @@
|
||||
"buttons":{
|
||||
"support": "Apoyo BSManager",
|
||||
"supporters":"Apoyos 👀"
|
||||
},
|
||||
"view":{
|
||||
"no-supporters": "Todavía no hay apoyos",
|
||||
"sponsors":"Patrocinadores",
|
||||
"supporters":"Apoyos"
|
||||
}
|
||||
},
|
||||
"contribution":{
|
||||
"description":"¡Sugiera nuevas características o reporte un error para ayudar a mejorar BSManager!",
|
||||
"buttons":{
|
||||
"request-features":"Proponer una funcionalidad",
|
||||
"report-bug":"Informar de un error",
|
||||
"open-logs": "Abrir los registros"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"notifications":{
|
||||
@@ -81,6 +112,11 @@
|
||||
"warning": "⚠️ Advertencia",
|
||||
"success": "🎉 Éxito"
|
||||
},
|
||||
"common":{
|
||||
"msg":{
|
||||
"error-occurred": "Se ha producido un error"
|
||||
}
|
||||
},
|
||||
"shared":{
|
||||
"errors":{
|
||||
"titles":{
|
||||
@@ -196,7 +232,44 @@
|
||||
"CloningFinished": "Clonación completa 🎉"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods":{
|
||||
"install-mods":{
|
||||
"titles":{
|
||||
"success": "Mods instalados 🎉",
|
||||
"warning": "Mods instalados 🤔"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Todos los mods han sido instalados.",
|
||||
"warning": "No se han podido instalar uno o más mods.",
|
||||
"errors":{
|
||||
"no-mods": "No hay que instalar mods.",
|
||||
"cannot-install-bsipa": "La instalación de BSIPA falló 😨"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"titles":{
|
||||
"success": "Mod desinstalado 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"errors":{
|
||||
"no-mods": "Este mod no está instalado 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"titles":{
|
||||
"success": "Mods desinstalados 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Todos los mods han sido desinstalados.",
|
||||
"errors": {
|
||||
"no-mods": "No se instala ningún mod en esta versión 😑"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals":{
|
||||
"guard":{
|
||||
@@ -256,7 +329,16 @@
|
||||
"buttons":{
|
||||
"submit": "Clonar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"title": "Desinstalar",
|
||||
"description": "¿Estás seguro de que quieres desinstalar {mod}? Podría provocar el mal funcionamiento de otros mods instalados.",
|
||||
"description-bsipa": "¿Estás seguro de que quieres desinstalar BSIPA? Después de eso, todos los mods instalados dejarán de funcionar."
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"title": "Desinstalación de mods",
|
||||
"description": "¿Estás seguro de que quieres desinstalar todos los mods de la versión {version}? Esta operación no se puede deshacer."
|
||||
}
|
||||
},
|
||||
"auto-update":{
|
||||
"checking": "Comprobar las actualizaciones",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"download": "Télécharger",
|
||||
"verify": "Vérifier",
|
||||
"launch": "Lancer",
|
||||
"mods": "Modes",
|
||||
"mods": "Mods",
|
||||
"maps": "Maps",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
@@ -14,6 +14,24 @@
|
||||
"desktop": "Mode Bureau",
|
||||
"debug": "Mode Debug"
|
||||
},
|
||||
"mods":{
|
||||
"loading-mods": "Chargement des mods...",
|
||||
"buttons":{
|
||||
"more-infos": "Plus d'infos",
|
||||
"install-or-update": "Installer ou mettre à jour"
|
||||
},
|
||||
"mods-grid":{
|
||||
"header-bar":{
|
||||
"name": "Nom",
|
||||
"installed": "Installé",
|
||||
"latest": "Récent",
|
||||
"description":"Description",
|
||||
"dropdown":{
|
||||
"uninstall-all":"Tout désinstaller"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown":{
|
||||
"open-folder": "Ouvrir le dossier",
|
||||
"verify-files": "Vérifier les fichiers",
|
||||
@@ -70,8 +88,20 @@
|
||||
"buttons":{
|
||||
"support": "Soutenir BSManager",
|
||||
"supporters":"Supporteurs 👀"
|
||||
},
|
||||
"view":{
|
||||
"no-supporters": "Pas encore de supporteurs",
|
||||
"sponsors":"Sponsors",
|
||||
"supporters":"Supporteurs"
|
||||
}
|
||||
},
|
||||
"contribution":{
|
||||
"description":"Propose de nouvelles fonctionnalités ou signale un bug pour contribuer à l'amélioration de BSManager !",
|
||||
"buttons":{
|
||||
"request-features":"Proposer une fonctionnalité",
|
||||
"report-bug":"Signaler un bug",
|
||||
"open-logs": "Ouvrir les logs"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81,6 +111,11 @@
|
||||
"warning": "⚠️ Attention",
|
||||
"success": "🎉 Succès"
|
||||
},
|
||||
"common":{
|
||||
"msg":{
|
||||
"error-occurred": "Une erreur c'est produite"
|
||||
}
|
||||
},
|
||||
"shared":{
|
||||
"errors":{
|
||||
"titles":{
|
||||
@@ -196,7 +231,44 @@
|
||||
"CloningFinished": "Clonage terminer 🎉"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods":{
|
||||
"install-mods":{
|
||||
"titles":{
|
||||
"success": "Mods installés 🎉",
|
||||
"warning": "Mods installés 🤔"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Tous les mods on été installés.",
|
||||
"warning": "Un ou plusieurs mods n'ont pas pu être installés.",
|
||||
"errors":{
|
||||
"no-mods": "Aucun mods à installer.",
|
||||
"cannot-install-bsipa": "L'installation de BSIPA à échoué 😨"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"titles":{
|
||||
"success": "Mod désintallé 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"errors":{
|
||||
"no-mods": "Ce mod n'est pas installé 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"titles":{
|
||||
"success": "Mods désintallés 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Tous les mods on été désintallés.",
|
||||
"errors": {
|
||||
"no-mods": "Aucun mod n'est installé dans dans cette version 😑"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals":{
|
||||
"guard":{
|
||||
@@ -232,7 +304,7 @@
|
||||
},
|
||||
"bs-uninstall":{
|
||||
"title": "Désinstaller",
|
||||
"description": "Est tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.",
|
||||
"description": "Est-tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.",
|
||||
"buttons":{
|
||||
"submit": "Désinstaller"
|
||||
}
|
||||
@@ -256,7 +328,16 @@
|
||||
"buttons":{
|
||||
"submit": "Cloner"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"title": "Désinstaller",
|
||||
"description": "Es-tu sûr de vouloir désinstaller {mod} ? Cela pourrait faire dysfonctionner d'autres mods installés.",
|
||||
"description-bsipa": "Es-tu sûr de vouloir désinstaller BSIPA ? Après cela, tous les mods installés ne fonctionneront plus."
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"title": "Désinstaller les mods",
|
||||
"description": "Es-tu sûr de vouloir désinstaller tous les mods de la version {version} ? Cette opération ne pourra pas être annulée."
|
||||
}
|
||||
},
|
||||
"auto-update":{
|
||||
"checking": "Vérification des mises à jour",
|
||||
|
||||
Generated
+70
-28
@@ -1861,6 +1861,15 @@
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"@types/use-double-click": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-double-click/-/use-double-click-1.0.1.tgz",
|
||||
"integrity": "sha512-mkoH+XW24GIPZO/0FO7ms6SiLxwvPPl8KxYxemoigSkPS9cQxI6CH6lw5gwQ5bLeProgzMGlULUANOnUCJ8GJg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||
@@ -3617,9 +3626,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"colord": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
|
||||
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
||||
"dev": true
|
||||
},
|
||||
"colorette": {
|
||||
@@ -4019,9 +4028,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"css-declaration-sorter": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.0.tgz",
|
||||
"integrity": "sha512-OGT677UGHJTAVMRhPO+HJ4oKln3wkBTwtDFH0ojbqm+MJm6xuDMHp2nkhh/ThaBqq20IbraBQSWKfSLNHQO9Og==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
|
||||
"integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==",
|
||||
"dev": true
|
||||
},
|
||||
"css-loader": {
|
||||
@@ -4041,14 +4050,14 @@
|
||||
}
|
||||
},
|
||||
"css-minimizer-webpack-plugin": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz",
|
||||
"integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.1.0.tgz",
|
||||
"integrity": "sha512-Zd+yz4nta4GXi3pMqF6skO8kjzuCUbr62z8SLMGZZtxWxTGTLopOiabPGNDEyjHCRhnhdA1EfHmqLa2Oekjtng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cssnano": "^5.0.6",
|
||||
"jest-worker": "^27.0.2",
|
||||
"postcss": "^8.3.5",
|
||||
"cssnano": "^5.1.8",
|
||||
"jest-worker": "^27.5.1",
|
||||
"postcss": "^8.4.13",
|
||||
"schema-utils": "^4.0.0",
|
||||
"serialize-javascript": "^6.0.0",
|
||||
"source-map": "^0.6.1"
|
||||
@@ -4151,9 +4160,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"cssnano": {
|
||||
"version": "5.1.12",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.12.tgz",
|
||||
"integrity": "sha512-TgvArbEZu0lk/dvg2ja+B7kYoD7BBCmn3+k58xD0qjrGHsFzXY/wKTo9M5egcUCabPol05e/PVoIu79s2JN4WQ==",
|
||||
"version": "5.1.13",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.13.tgz",
|
||||
"integrity": "sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cssnano-preset-default": "^5.2.12",
|
||||
@@ -9024,6 +9033,11 @@
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"md5-file": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz",
|
||||
"integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw=="
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
@@ -9389,6 +9403,11 @@
|
||||
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
||||
"dev": true
|
||||
},
|
||||
"node-stream-zip": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
|
||||
"integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="
|
||||
},
|
||||
"nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
@@ -10771,6 +10790,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-visibility-sensor": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz",
|
||||
"integrity": "sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -11184,9 +11211,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
@@ -11488,6 +11515,14 @@
|
||||
"faye-websocket": "^0.11.3",
|
||||
"uuid": "^8.3.2",
|
||||
"websocket-driver": "^0.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"socks": {
|
||||
@@ -11853,13 +11888,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"tailwind-scrollbar": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-1.3.1.tgz",
|
||||
"integrity": "sha512-FeYuLxLtCRMO4PmjPJCzm5wQouFro2BInZXKPxqg54DR/55NAHoS8uNYWMiRG5l6qsLkWBfVEM34gq2XAQUwVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tailwindcss": ">1.9.6"
|
||||
}
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz",
|
||||
"integrity": "sha512-OcR7qHBbux4k+k6bWqnEQFYFooLK/F4dhkBz6nvswIoaA9ancZ5h20e0tyV7ifSWLDCUBtpG+1NHRA8HMRH/wg==",
|
||||
"dev": true
|
||||
},
|
||||
"tailwind-scrollbar-hide": {
|
||||
"version": "1.1.7",
|
||||
@@ -11896,6 +11928,11 @@
|
||||
"resolve": "^1.22.1"
|
||||
}
|
||||
},
|
||||
"tailwindcss-scoped-groups": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss-scoped-groups/-/tailwindcss-scoped-groups-2.0.0.tgz",
|
||||
"integrity": "sha512-uE9PJzfjqzsKlZ0dUjG5yPwBmkEZ/qmrXMLjykRs0otolUHAIclbtV49JQGyeYLo8YA7WPMgzKOg2e4snVyHqw=="
|
||||
},
|
||||
"tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
@@ -12531,6 +12568,11 @@
|
||||
"prepend-http": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"use-double-click": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/use-double-click/-/use-double-click-1.0.5.tgz",
|
||||
"integrity": "sha512-71LUca6NtzpzHYlcfM/dOdmwvmvpMbzeIVQpN87w+DctpLiMCXtZpsN8FNWPgHpPBtNhvucPUHIDh5al8D8C7w=="
|
||||
},
|
||||
"utf8-byte-length": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
|
||||
@@ -12555,9 +12597,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
|
||||
+10
-3
@@ -157,6 +157,7 @@
|
||||
"@types/react-outside-click-handler": "^1.3.1",
|
||||
"@types/react-test-renderer": "^17.0.2",
|
||||
"@types/terser-webpack-plugin": "^5.0.4",
|
||||
"@types/use-double-click": "^1.0.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webpack-bundle-analyzer": "^4.4.2",
|
||||
"@types/webpack-env": "^1.18.0",
|
||||
@@ -169,7 +170,7 @@
|
||||
"core-js": "^3.24.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||
"css-minimizer-webpack-plugin": "^4.1.0",
|
||||
"detect-port": "^1.3.0",
|
||||
"electron": "^20.0.3",
|
||||
"electron-builder": "^23.3.3",
|
||||
@@ -206,7 +207,7 @@
|
||||
"sass": "^1.54.5",
|
||||
"sass-loader": "^12.6.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwind-scrollbar": "^1.3.1",
|
||||
"tailwind-scrollbar": "^2.0.1",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"terser-webpack-plugin": "^5.3.5",
|
||||
"ts-jest": "^27.1.5",
|
||||
@@ -232,16 +233,22 @@
|
||||
"framer-motion": "^7.2.1",
|
||||
"history": "^5.3.0",
|
||||
"is-online": "^9.0.1",
|
||||
"md5-file": "^5.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fitty": "^1.0.1",
|
||||
"react-outside-click-handler": "^1.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-visibility-sensor": "^5.1.1",
|
||||
"regedit": "^5.1.1",
|
||||
"rxjs": "^7.5.6",
|
||||
"semver": "^7.3.8",
|
||||
"tailwind-scrollbar-hide": "^1.1.7",
|
||||
"uuid": "^8.3.2"
|
||||
"tailwindcss-scoped-groups": "^2.0.0",
|
||||
"use-double-click": "^1.0.5",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": ">=14.x",
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "BSManager",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "BSManager",
|
||||
"version": "0.0.8",
|
||||
"name": "bs-manager",
|
||||
"version": "0.2.0",
|
||||
"description": "A foundation for scalable desktop apps",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -10,7 +10,6 @@ ipcMain.on('bs-launch.launch', (event, request: IpcRequest<LauchOption>) => {
|
||||
const utilsService = UtilsService.getInstance();
|
||||
|
||||
launcherService.launch(request.args).then(res => {
|
||||
console.log("BEAT SABER LANCER");
|
||||
utilsService.ipcSend(request.responceChannel, {success: true, data: res});
|
||||
}).catch((err: BsmException) => {
|
||||
utilsService.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { BsModsManagerService } from "../services/mods/bs-mods-manager.service";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { InstallModsResult } from "shared/models/mods";
|
||||
import log from "electron-log";
|
||||
|
||||
ipcMain.on("get-available-mods", (event, request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.getAvailableMods(request.args).then(mods => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: mods});
|
||||
}).catch(err =>{
|
||||
utils.ipcSend(request.responceChannel, {success: false});
|
||||
log.error("ipc", "get-available-mods", err, request);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("get-installed-mods", (event, request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.getInstalledMods(request.args).then(mods => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: mods});
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false});
|
||||
log.error("ipc", "get-installed-mods", err, request);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("install-mods", (event, request: IpcRequest<{mods: Mod[], version: BSVersion}>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.installMods(request.args.mods, request.args.version).then(nbInstalled => {
|
||||
utils.ipcSend<InstallModsResult>(request.responceChannel, {success: true, data: nbInstalled})
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
log.error("ipc", "install-mods", err, request);
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.on("uninstall-mods", (event, request: IpcRequest<{mods: Mod[], version: BSVersion}>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.uninstallMods(request.args.mods, request.args.version).then(nbInstalled => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: nbInstalled})
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
log.error("ipc", "uninstall-mods", err, request);
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.on("uninstall-all-mods", (event, request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.uninstallAllMods(request.args).then(nbInstalled => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: nbInstalled})
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
log.error("ipc", "uninstall-all-mods", err, request);
|
||||
})
|
||||
});
|
||||
@@ -7,3 +7,4 @@ import './map-ipcs';
|
||||
import './supporters-ipcs';
|
||||
import './launcher-ipcs';
|
||||
import './window-manager-ipcs';
|
||||
import './bs-mods-ipcs';
|
||||
|
||||
@@ -48,3 +48,7 @@ ipcMain.on('save-file', async (event, request: IpcRequest<{filename?: string, fi
|
||||
ipcMain.on("current-version", async (event, request: IpcRequest<void>) => {
|
||||
UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: app.getVersion()});
|
||||
});
|
||||
|
||||
ipcMain.on("open-logs", async (event, request: IpcRequest<void>) => {
|
||||
shell.openPath(app.getPath("logs"));
|
||||
});
|
||||
|
||||
+1
-2
@@ -30,7 +30,6 @@ log.catchErrors();
|
||||
const utilsService = UtilsService.getInstance();
|
||||
utilsService.setAssetsPath(app.isPackaged ? path.join(process.resourcesPath, 'assets') : path.join(__dirname, '../../assets'));
|
||||
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const sourceMapSupport = require('source-map-support');
|
||||
sourceMapSupport.install();
|
||||
@@ -63,4 +62,4 @@ app.on('window-all-closed', () => {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
}).catch(console.log);
|
||||
}).catch(log.error);
|
||||
@@ -1,6 +1,7 @@
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import log from 'electron-log';
|
||||
import { UtilsService } from './utils.service';
|
||||
import { gt } from 'semver';
|
||||
export class AutoUpdaterService {
|
||||
|
||||
private static instance: AutoUpdaterService;
|
||||
@@ -24,8 +25,7 @@ export class AutoUpdaterService {
|
||||
autoUpdater.checkForUpdates().then(info => {
|
||||
const needUpdate = (() => {
|
||||
if(!info || !info.updateInfo){ return false; }
|
||||
if(autoUpdater.currentVersion.version === info.updateInfo.version){ return false; }
|
||||
return true;
|
||||
return gt(info.updateInfo.version, autoUpdater.currentVersion.version);
|
||||
})();
|
||||
resolve(needUpdate)}
|
||||
).catch(() => resolve(false));
|
||||
@@ -38,10 +38,6 @@ export class AutoUpdaterService {
|
||||
autoUpdater.addListener("download-progress", info => {
|
||||
this.utilsService.ipcSend("update-download-progress", {success: true, data: info.percent});
|
||||
});
|
||||
// const promise = new Promise<boolean>((resolve, reject) => {
|
||||
// autoUpdater.addListener("update-downloaded", () => resolve(true));
|
||||
// autoUpdater.addListener("error", () => reject(false))
|
||||
// });
|
||||
|
||||
return autoUpdater.downloadUpdate().then(res => !!res && !!res.length);
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ export class BSLauncherService{
|
||||
const cwd = await this.localVersionService.getVersionPath(launchOptions.version);
|
||||
const exePath = path.join(cwd, BS_EXECUTABLE);
|
||||
|
||||
console.log(exePath);
|
||||
|
||||
if(!this.utilsService.pathExist(exePath)){ return "EXE_NOT_FINDED"; }
|
||||
|
||||
const launchMods = [launchOptions.oculus && "-vrmode oculus", launchOptions.desktop && "fpfc", launchOptions.debug && "--verbose"];
|
||||
|
||||
@@ -11,6 +11,7 @@ import fs from "fs-extra";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { rename } from "fs/promises";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import log from "electron-log";
|
||||
|
||||
export class BSLocalVersionService{
|
||||
|
||||
@@ -90,13 +91,18 @@ export class BSLocalVersionService{
|
||||
return seq.replace( /[<>:"\/\\|?*]+/g, '' );
|
||||
}
|
||||
|
||||
public getVersionFolder(version: BSVersion){
|
||||
return version.name ? `${version.BSVersion}-${version.name}` : version.BSVersion;
|
||||
}
|
||||
public getVersionFolder(version: BSVersion){
|
||||
return version.name ? `${version.BSVersion}-${version.name}` : version.BSVersion;
|
||||
}
|
||||
|
||||
public async getInstalledVersions(): Promise<BSVersion[]>{
|
||||
public getVersionType(version: BSVersion): "steam"|"universal"{
|
||||
if(version.steam){ return "steam"; }
|
||||
return "universal"
|
||||
}
|
||||
|
||||
public async getInstalledVersions(): Promise<BSVersion[]>{
|
||||
const versions: BSVersion[] = [];
|
||||
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber")
|
||||
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
|
||||
if(steamBsFolder && this.utilsService.pathExist(steamBsFolder)){
|
||||
const steamBsVersion = await this.getVersionOfBSFolder(steamBsFolder);
|
||||
if(steamBsVersion){
|
||||
@@ -108,6 +114,7 @@ export class BSLocalVersionService{
|
||||
|
||||
const folderInInstallation = this.utilsService.listDirsInDir(this.installLocationService.versionsDirectory);
|
||||
folderInInstallation.forEach(f => {
|
||||
log.info("try get version from folder", f);
|
||||
let version = this.remoteVersionService.getVersionDetails(f);
|
||||
if(version){ version = this.getCustomVersions().find(v => v.BSVersion === version.BSVersion && v.name === version.name) ?? version; }
|
||||
else { version = this.getCustomVersions().find(v => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UtilsService } from './utils.service';
|
||||
import path from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { RequestService } from "./request.service"
|
||||
import isOnline from 'is-online';
|
||||
|
||||
export class BSVersionLibService{
|
||||
@@ -13,11 +14,13 @@ export class BSVersionLibService{
|
||||
private static instance: BSVersionLibService;
|
||||
|
||||
private utilsService: UtilsService;
|
||||
private requestService: RequestService
|
||||
|
||||
private bsVersions: BSVersion[] = [];
|
||||
|
||||
private constructor(){
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,19 +29,9 @@ export class BSVersionLibService{
|
||||
return BSVersionLibService.instance;
|
||||
}
|
||||
|
||||
private getRemoteVersions(): Promise<BSVersion[]>{
|
||||
return new Promise<BSVersion[]>((resolve, reject) => {
|
||||
let body = ''
|
||||
get(this.REMOTE_BS_VERSIONS_URL, (res) => {
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
this.bsVersions = JSON.parse(body);
|
||||
resolve(this.bsVersions);
|
||||
});
|
||||
res.on('error', () => reject(null))
|
||||
})
|
||||
})
|
||||
}
|
||||
private getRemoteVersions(): Promise<BSVersion[]>{
|
||||
return this.requestService.get<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL);
|
||||
}
|
||||
|
||||
private async getLocalVersions(): Promise<BSVersion[]>{
|
||||
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { RequestService } from "../request.service";
|
||||
|
||||
export class BeatModsApiService {
|
||||
|
||||
private static instance: BeatModsApiService;
|
||||
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private readonly BEAT_MODS_VERSIONS = "https://versions.beatmods.com/versions.json";
|
||||
private readonly BEAT_MODS_ALIAS = "https://alias.beatmods.com/aliases.json";
|
||||
|
||||
private readonly BEAT_MODS_API_URL = "https://beatmods.com/api/v1/";
|
||||
public readonly BEAT_MODS_URL = "https://beatmods.com";
|
||||
|
||||
private readonly aliasesCache = new Map<string, BSVersion[]>();
|
||||
private readonly versionModsCache = new Map<string, Mod[]>();
|
||||
|
||||
private allModsCache: Mod[];
|
||||
|
||||
public static getInstance(): BeatModsApiService{
|
||||
if(!BeatModsApiService.instance){ BeatModsApiService.instance = new BeatModsApiService(); }
|
||||
return BeatModsApiService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
private getVersionModsUrl(version: BSVersion): string{
|
||||
return `${this.BEAT_MODS_API_URL}mod?status=approved&gameVersion=${version.BSVersion}&sort=&sortDirection=1`;
|
||||
}
|
||||
|
||||
private getAllModsUrl(): string{
|
||||
return `${this.BEAT_MODS_API_URL}mod`;
|
||||
}
|
||||
|
||||
private async getVersionAlias(): Promise<Map<string, BSVersion[]>>{
|
||||
if(this.aliasesCache.size){ return this.aliasesCache; }
|
||||
return this.requestService.get<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(rawAliases => {
|
||||
Object.entries(rawAliases).forEach(([key, value]) => {
|
||||
this.aliasesCache.set(key, value.map(s => ({BSVersion: s} as BSVersion)));
|
||||
});
|
||||
return this.aliasesCache;
|
||||
})
|
||||
}
|
||||
|
||||
private async getAliasOfVersion(version: BSVersion): Promise<BSVersion>{
|
||||
return this.getVersionAlias().then(aliases => {
|
||||
if(Array.from(aliases.keys()).some(k => k === version.BSVersion)){ return version; }
|
||||
const alias = Array.from(aliases.entries()).find(([key, value]) => value.find(v => v.BSVersion === version.BSVersion))[0];
|
||||
return {BSVersion: alias} as BSVersion
|
||||
});
|
||||
}
|
||||
|
||||
private asignDependencies(mod: Mod, mods: Mod[]): Mod{
|
||||
mod.dependencies = mod.dependencies.map(dep => mods.find(mod => mod.name === dep.name));
|
||||
return mod;
|
||||
}
|
||||
|
||||
public async getVersionMods(version: BSVersion): Promise<Mod[]>{
|
||||
if(this.versionModsCache.has(version.BSVersion)){ return this.versionModsCache.get(version.BSVersion); }
|
||||
|
||||
const alias = await this.getAliasOfVersion(version);
|
||||
|
||||
return this.requestService.get<Mod[]>(this.getVersionModsUrl(alias)).then(mods => {
|
||||
mods = mods.map(mod => this.asignDependencies(mod, mods));
|
||||
this.versionModsCache.set(version.BSVersion, mods);
|
||||
return mods;
|
||||
});
|
||||
}
|
||||
|
||||
public async getAllMods(): Promise<Mod[]>{
|
||||
if(!!this.allModsCache){ return this.allModsCache; }
|
||||
return this.requestService.get<Mod[]>(this.getAllModsUrl()).then(mods => {
|
||||
this.allModsCache = mods;
|
||||
return this.allModsCache;
|
||||
});
|
||||
}
|
||||
|
||||
public loadAllMods(){ return this.getAllMods(); }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { DownloadLink, InstallModsResult, Mod, ModInstallProgression, UninstallModsResult } from "shared/models/mods";
|
||||
import { BeatModsApiService } from "./beat-mods-api.service";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service"
|
||||
import path from "path";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import md5File from "md5-file";
|
||||
import fs from "fs"
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../request.service";
|
||||
import { spawn } from "child_process";
|
||||
import { BS_EXECUTABLE } from "../../constants";
|
||||
import log from "electron-log";
|
||||
|
||||
export class BsModsManagerService {
|
||||
|
||||
private static instance: BsModsManagerService;
|
||||
|
||||
private readonly beatModsApi: BeatModsApiService;
|
||||
private readonly bsLocalService: BSLocalVersionService;
|
||||
private readonly utilsService: UtilsService;
|
||||
private readonly requestService: RequestService
|
||||
|
||||
private manifestMatches: Mod[];
|
||||
|
||||
private nbModsToInstall = 0;
|
||||
private nbInstalledMods = 0;
|
||||
|
||||
private nbModsToUninstall = 0;
|
||||
private nbUninstalledMods = 0;
|
||||
|
||||
public static getInstance(): BsModsManagerService{
|
||||
if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); }
|
||||
return BsModsManagerService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.beatModsApi = BeatModsApiService.getInstance();
|
||||
this.bsLocalService = BSLocalVersionService.getInstance();
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
private async getModFromHash(hash: string): Promise<Mod>{
|
||||
const allMods = await this.beatModsApi.getAllMods();
|
||||
return allMods.find(mod => {
|
||||
if(mod.name.toLowerCase() === "bsipa" || mod.status === "declined"){ return false; }
|
||||
return mod.downloads.some(download => download.hashMd5.some(md5 => md5.hash === hash));
|
||||
})
|
||||
}
|
||||
|
||||
private async getIpaFromHash(hash: string): Promise<Mod>{
|
||||
const allMods = await this.beatModsApi.getAllMods();
|
||||
return allMods.find(mod => {
|
||||
if(mod.name.toLowerCase() !== "bsipa"){ return false; }
|
||||
return mod.downloads.some(download => download.hashMd5.some(md5 => md5.hash === hash));
|
||||
})
|
||||
}
|
||||
|
||||
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<Mod[]>{
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const modsPath = path.join(bsPath, modsDir);
|
||||
if(!this.utilsService.pathExist(modsPath)){ return []; }
|
||||
const files = fs.readdirSync(modsPath);
|
||||
const promises = files.map(f => {
|
||||
return (async() => {
|
||||
const filePath = path.join(modsPath, f)
|
||||
const ext = path.extname(f);
|
||||
if(ext !== ".dll" && ext !== ".exe" && ext !== ".manifest"){ return undefined; }
|
||||
const hash = await md5File(filePath);
|
||||
const mod = await this.getModFromHash(hash);
|
||||
if(!mod){ return undefined; }
|
||||
if(ext === ".manifest"){
|
||||
this.manifestMatches.push(mod);
|
||||
return undefined;
|
||||
}
|
||||
if(filePath.includes("Libs")){
|
||||
if(!this.manifestMatches.some(m => m.name === mod.name)){ return undefined; }
|
||||
const modIndex = this.manifestMatches.indexOf(mod);
|
||||
if(modIndex > -1){ this.manifestMatches.splice(modIndex, 1); }
|
||||
}
|
||||
return mod;
|
||||
})()
|
||||
});
|
||||
const mods = await Promise.all(promises)
|
||||
return mods.filter(m => !!m);
|
||||
}
|
||||
|
||||
private async getBsipaInstalled(version: BSVersion): Promise<Mod>{
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const injectorPath = path.join(bsPath, "Beat Saber_Data", "Managed", "IPA.Injector.dll");
|
||||
if(!this.utilsService.pathExist(injectorPath)){ return undefined; }
|
||||
const injectorMd5 = await md5File(injectorPath);
|
||||
return this.getIpaFromHash(injectorMd5);
|
||||
}
|
||||
|
||||
private async downloadZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{
|
||||
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
|
||||
const fileName = path.basename(zipUrl);
|
||||
const tempPath = this.utilsService.getTempPath();
|
||||
this.utilsService.createFolderIfNotExist(this.utilsService.getTempPath());
|
||||
const dest = path.join(tempPath, fileName);
|
||||
|
||||
const zipPath = await this.requestService.downloadFile(zipUrl, dest);
|
||||
const zip = new StreamZip.async({file : zipPath});
|
||||
|
||||
return {zip, zipPath};
|
||||
}
|
||||
|
||||
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean>{
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const ipaPath = path.join(versionPath, "IPA.exe");
|
||||
const bsExePath = path.join(versionPath, BS_EXECUTABLE);
|
||||
if(!this.utilsService.pathExist(ipaPath) || !this.utilsService.pathExist(bsExePath)){ return false; }
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
const processIPA = spawn(`start /wait /min "" "${ipaPath}" ${args.join(" ")}`, {cwd: versionPath, detached: true, shell: true});
|
||||
processIPA.once("exit", code => {
|
||||
if(code === 0){ return resolve(true); }
|
||||
log.error("IPA PROCESS", "exit code", code);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
setTimeout(() => resolve(false), (1 * 60) * 1000); //timeout 1min
|
||||
});
|
||||
}
|
||||
|
||||
private getModDownload(mod: Mod, version: BSVersion): DownloadLink{
|
||||
return mod.downloads.find(download => {
|
||||
const type = download.type.toLowerCase()
|
||||
return type === "universal" || type === this.bsLocalService.getVersionType(version);
|
||||
});
|
||||
}
|
||||
|
||||
private async installMod(mod: Mod, version: BSVersion): Promise<boolean>{
|
||||
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", {success: true, data: {name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100}})
|
||||
|
||||
const download = this.getModDownload(mod, version);
|
||||
|
||||
if(!download){ return false; }
|
||||
|
||||
const {zip, zipPath} = await this.downloadZip(download.url);
|
||||
|
||||
if(!zip){ return false; }
|
||||
|
||||
const crypto = require('crypto');
|
||||
const entries = await zip.entries();
|
||||
|
||||
const checkedEntries = (await Promise.all(Object.values(entries).map(async (entry) => {
|
||||
if(!entry.isFile){ return undefined; }
|
||||
const data = await zip.entryData(entry);
|
||||
const entryMd5 = crypto.createHash('md5').update(data).digest('hex')
|
||||
return download.hashMd5.some(md5 => md5.hash === entryMd5) ? entry : undefined;
|
||||
}))).filter(entry => !!entry);
|
||||
|
||||
if(checkedEntries.length != download.hashMd5.length){ return false; }
|
||||
|
||||
const verionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const isBSIPA = mod.name.toLowerCase() === "bsipa";
|
||||
const destDir = isBSIPA ? verionPath : path.join(verionPath, ModsInstallFolder.PENDING);
|
||||
|
||||
const extracted = await zip.extract(null, destDir).then(() => true).catch(err => {log.error(err); return false});
|
||||
|
||||
await zip.close();
|
||||
await this.utilsService.unlinkIfExist(zipPath);
|
||||
|
||||
const res = isBSIPA ? (extracted && (await this.executeBSIPA(version, ["-n"]))) : extracted;
|
||||
|
||||
res && this.nbInstalledMods++;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private isDependency(mod: Mod, selectedMods: Mod[], availableMods: Mod[]){
|
||||
return selectedMods.some(m => {
|
||||
const deps = m.dependencies.map(dep => Array.from(availableMods.values()).find(m => dep.name === m.name));
|
||||
if(deps.some(depMod => depMod.name === mod.name)){ return true; }
|
||||
return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]>{
|
||||
const availableMods = await this.beatModsApi.getVersionMods(version);
|
||||
return Array.from(new Map<string, Mod>(availableMods.reduce((res, mod) => {
|
||||
if(this.isDependency(mod, mods, availableMods)){
|
||||
res.push([mod.name, mod]);
|
||||
}
|
||||
return res;
|
||||
}, [])).values());
|
||||
}
|
||||
|
||||
private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise<void>{
|
||||
const download = this.getModDownload(mod, version);
|
||||
|
||||
const verionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const hasIPAExe = this.utilsService.pathExist(path.join(verionPath, "IPA.exe"));
|
||||
const hasIPADir = this.utilsService.pathExist(path.join(verionPath, "IPA"));
|
||||
|
||||
if(!hasIPADir || !hasIPAExe){ return; }
|
||||
|
||||
await this.executeBSIPA(version, ["--revert", "-n"]);
|
||||
|
||||
const promises = download.hashMd5.map(files => {
|
||||
const file = files.file.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
|
||||
return this.utilsService.unlinkIfExist(path.join(verionPath, file));
|
||||
})
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void>{
|
||||
|
||||
this.nbUninstalledMods++;
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", {success: true, data: {name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100}})
|
||||
|
||||
if(mod.name.toLowerCase() === "bsipa"){ return this.uninstallBSIPA(mod, version); }
|
||||
|
||||
const download = this.getModDownload(mod, version);
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
|
||||
const promises = download.hashMd5.map(async files => {
|
||||
this.utilsService.unlinkIfExist(path.join(versionPath, files.file));
|
||||
this.utilsService.unlinkIfExist(path.join(versionPath, "IPA", "Pending", files.file));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Promise<Mod[]>{
|
||||
return this.beatModsApi.getVersionMods(version);
|
||||
}
|
||||
|
||||
public async getInstalledMods(version: BSVersion): Promise<Mod[]>{
|
||||
this.manifestMatches = [];
|
||||
await this.beatModsApi.loadAllMods();
|
||||
const bsipa = await this.getBsipaInstalled(version);
|
||||
return Promise.all([
|
||||
this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING),
|
||||
this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING),
|
||||
this.getModsInDir(version, ModsInstallFolder.PLUGINS),
|
||||
this.getModsInDir(version, ModsInstallFolder.LIBS)
|
||||
]).then(dirMods => {
|
||||
const res = [];
|
||||
if(!!bsipa){ res.push(bsipa); }
|
||||
const installedMods = Array.from(new Map<string, Mod>(dirMods.flat().map(m => [m.name, m])).values());
|
||||
res.push(...installedMods);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult>{
|
||||
if(!mods || !mods.length){ throw "no-mods"; }
|
||||
|
||||
const deps = await this.resolveDependencies(mods, version);
|
||||
mods.push(...deps);
|
||||
|
||||
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
|
||||
if(bsipa){ mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa"); }
|
||||
|
||||
this.nbModsToInstall = mods.length + (bsipa && 1);
|
||||
this.nbInstalledMods = 0;
|
||||
|
||||
if(bsipa){
|
||||
const installed = await this.installMod(bsipa, version).catch(err => {log.error("INSTALL BSIPA", err); return false; })
|
||||
if(!installed){ throw "cannot-install-bsipa"; }
|
||||
}
|
||||
|
||||
for(const mod of mods){
|
||||
await this.installMod(mod, version);;
|
||||
}
|
||||
|
||||
return {
|
||||
nbModsToInstall: this.nbModsToInstall,
|
||||
nbInstalledMods: this.nbInstalledMods
|
||||
};
|
||||
}
|
||||
|
||||
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<UninstallModsResult>{
|
||||
if(!mods || !mods.length){ throw "no-mods"; }
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
this.nbUninstalledMods = 0;
|
||||
|
||||
for(const mod of mods){
|
||||
await this.uninstallMod(mod, version);
|
||||
}
|
||||
|
||||
return {
|
||||
nbModsToUninstall: this.nbModsToUninstall,
|
||||
nbUninstalledMods: this.nbUninstalledMods
|
||||
};
|
||||
}
|
||||
|
||||
public async uninstallAllMods(version: BSVersion): Promise<UninstallModsResult>{
|
||||
const mods = await this.getInstalledMods(version);
|
||||
|
||||
if(!mods || !mods.length){ throw "no-mods"; }
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
this.nbUninstalledMods = 0;
|
||||
|
||||
for(const mod of mods){
|
||||
await this.uninstallMod(mod, version);
|
||||
}
|
||||
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
|
||||
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.PLUGINS));
|
||||
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.LIBS));
|
||||
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.IPA));
|
||||
|
||||
path.resolve
|
||||
|
||||
return {
|
||||
nbModsToUninstall: this.nbModsToUninstall,
|
||||
nbUninstalledMods: this.nbUninstalledMods
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const enum ModsInstallFolder {
|
||||
PLUGINS = "Plugins",
|
||||
LIBS = "Libs",
|
||||
IPA = "IPA",
|
||||
PENDING = "IPA/Pending",
|
||||
PLUGINS_PENDING = "IPA/Pending/Plugins",
|
||||
LIBS_PENDING = "IPA/Pending/Libs"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { get } from "https";
|
||||
import { createWriteStream, unlink } from "fs";
|
||||
|
||||
export class RequestService {
|
||||
|
||||
private static instance: RequestService;
|
||||
|
||||
public static getInstance(): RequestService{
|
||||
if(!RequestService.instance){ RequestService.instance = new RequestService(); }
|
||||
return RequestService.instance;
|
||||
}
|
||||
|
||||
private constructor(){}
|
||||
|
||||
public get<T = any>(url: string): Promise<T>{
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = ''
|
||||
get(url, (res) => {
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
resolve(JSON.parse(body));
|
||||
});
|
||||
res.on('error', (err) => reject(err))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public downloadFile(url: string, dest: string): Promise<string>{
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(dest);
|
||||
get(url, res => {
|
||||
res.pipe(file);
|
||||
file.on("close", () => {
|
||||
file.close(() => resolve(dest))
|
||||
}).on("error", err => {
|
||||
unlink(dest, () => reject(err));
|
||||
});
|
||||
}).on("error", err => {
|
||||
unlink(dest, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,27 +27,31 @@ export class SteamService{
|
||||
}
|
||||
|
||||
public async getSteamPath(): Promise<string>{
|
||||
if(this.steamPath !== ''){ return this.steamPath; }
|
||||
|
||||
if(!!this.steamPath){ return this.steamPath; }
|
||||
|
||||
const [win32Res, win64Res] = await Promise.all([
|
||||
regedit.promisified.list(['HKLM\\SOFTWARE\\Valve\\Steam']),
|
||||
regedit.promisified.list(['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam'])
|
||||
]);
|
||||
|
||||
const [win32, win64] = [win32Res["HKLM\\SOFTWARE\\Valve\\Steam"], win64Res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]];
|
||||
let res = ''
|
||||
if(win64.values.InstallPath.value){ res = win64.values.InstallPath.value as string; }
|
||||
else if(win32.values.InstallPath.value){ res = win32.values.InstallPath.value as string; }
|
||||
|
||||
let res = '';
|
||||
if(win64.exists && win64?.values?.InstallPath?.value){ res = win64.values.InstallPath.value as string; }
|
||||
else if(win32.exists && win32?.values?.InstallPath?.value){ res = win32.values.InstallPath.value as string; }
|
||||
this.steamPath = res;
|
||||
return this.steamPath;
|
||||
}
|
||||
|
||||
public async getGameFolder(gameId: string, gameFolder?: string): Promise<string>{
|
||||
const steamPath = await this.getSteamPath();
|
||||
|
||||
let libraryFolders: any = path.join(steamPath, 'steamapps', 'libraryfolders.vdf');
|
||||
|
||||
if(!this.utils.pathExist(libraryFolders)){ return null; }
|
||||
libraryFolders = parse(await readFile(libraryFolders, {encoding: 'utf-8'}));
|
||||
|
||||
|
||||
if(!libraryFolders.libraryfolders){ return null; }
|
||||
libraryFolders = libraryFolders.libraryfolders
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { existsSync, mkdirSync, readdirSync, readFile } from "fs";
|
||||
import { existsSync, mkdirSync, readdirSync, readFile, unlinkSync } from "fs";
|
||||
import { spawnSync } from "child_process";
|
||||
import { homedir } from "os";
|
||||
import path from "path";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { rm } from "fs/promises";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { rm, unlink } from "fs/promises";
|
||||
import { IpcResponse } from "shared/models/ipc";
|
||||
import log from "electron-log";
|
||||
|
||||
@@ -27,6 +27,7 @@ export class UtilsService{
|
||||
|
||||
public getAssetsScriptsPath(): string { return this.getAssetsPath("scripts") }
|
||||
public getAssestsJsonsPath(): string { return this.getAssetsPath("jsons"); }
|
||||
public getTempPath(): string{ return path.join(app.getPath("temp"), app.getName()) }
|
||||
|
||||
public setMainWindow(win: BrowserWindow){ this.mainWindow = win; }
|
||||
public getMainWindow(){ return this.mainWindow; }
|
||||
@@ -37,6 +38,16 @@ export class UtilsService{
|
||||
if(!this.pathExist(path)){ mkdirSync(path, {recursive: true}); }
|
||||
}
|
||||
|
||||
public async unlinkIfExist(pathToFile: string): Promise<void>{
|
||||
if(!this.pathExist(pathToFile)){ return }
|
||||
return unlink(pathToFile);
|
||||
}
|
||||
|
||||
public rmDirIfExist(path: string): Promise<void>{
|
||||
if(!this.pathExist(path)){ return; }
|
||||
return rm(path, {recursive: true, force: true});
|
||||
}
|
||||
|
||||
public taskRunning(task: string): boolean{
|
||||
const tasks = spawnSync('tasklist').stdout.toString();
|
||||
return tasks.includes(task);
|
||||
@@ -67,7 +78,7 @@ export class UtilsService{
|
||||
return rm(folderPath, {recursive: true});
|
||||
}
|
||||
|
||||
public ipcSend(channel: string, response: IpcResponse<any>): void{
|
||||
public ipcSend<T = any>(channel: string, response: IpcResponse<T>): void{
|
||||
try {
|
||||
this.mainWindow.webContents.send(channel, response);
|
||||
} catch (error) {
|
||||
@@ -75,4 +86,6 @@ export class UtilsService{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+14
-8
@@ -1,6 +1,6 @@
|
||||
import { NavBar } from "./components/nav-bar/nav-bar.component";
|
||||
import TitleBar from "./components/title-bar/title-bar.component";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { Routes, Route, useLocation } from "react-router-dom";
|
||||
import { AvailableVersionsList } from "./pages/available-versions-list.components";
|
||||
import { VersionViewer } from "./pages/version-viewer.component";
|
||||
import { Modal } from "./components/modal/modal.component";
|
||||
@@ -9,17 +9,23 @@ import { BsmProgressBar } from "./components/progress-bar/bsm-progress-bar.compo
|
||||
import { useEffect } from "react";
|
||||
import { ThemeService } from "./services/theme.service";
|
||||
import { NotificationOverlay } from "./components/notification/notification-overlay.component";
|
||||
import { PageStateService } from "./services/page-state.service";
|
||||
|
||||
export default function App() {
|
||||
|
||||
const themeService = ThemeService.getInstance();
|
||||
const themeService = ThemeService.getInstance();
|
||||
const pageState = PageStateService.getInstance();
|
||||
|
||||
useEffect(() => {
|
||||
themeService.theme$.subscribe(() => {
|
||||
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ document.documentElement.classList.add('dark'); }
|
||||
else { document.documentElement.classList.remove('dark'); }
|
||||
});
|
||||
}, []);
|
||||
const location = useLocation();
|
||||
|
||||
pageState.setState(location.state);
|
||||
|
||||
useEffect(() => {
|
||||
themeService.theme$.subscribe(() => {
|
||||
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ return document.documentElement.classList.add('dark'); }
|
||||
document.documentElement.classList.remove('dark');
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
.glow-on-hover {
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(45deg, #ff0000, #ff7300, #fffb00, #48ff00, #00ffd5, #002bff, #7a00ff, #ff00c8, #ff0000);
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left:-4px;
|
||||
background-size: 400%;
|
||||
z-index: -1;
|
||||
filter: blur(3px);
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
animation: glowing 20s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-in-out;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@keyframes glowing {
|
||||
0% { background-position: 0 0; }
|
||||
50% { background-position: 400% 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import './available-version-item.component.css';
|
||||
import { useEffect, useState, memo } from "react";
|
||||
import { BsDownloaderService } from "renderer/services/bs-downloader.service";
|
||||
import { distinctUntilChanged } from "rxjs/operators";
|
||||
import defaultImage from '../../../../assets/images/default-version-img.jpg'
|
||||
import dateFormat from "dateformat";
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component";
|
||||
import { LinkOpenerService } from 'renderer/services/link-opener.service';
|
||||
|
||||
export const AvailableVersionItem = memo(function AvailableVersionItem(props: {version: BSVersion}) {
|
||||
|
||||
const bsDownloaderService = BsDownloaderService.getInstance();
|
||||
const ipcService = IpcService.getInstance();
|
||||
const linkOpener = LinkOpenerService.getInstance();
|
||||
|
||||
const [selected, setSelected] = useState(false);
|
||||
const t = useTranslation();
|
||||
@@ -26,7 +25,7 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {v
|
||||
else{ bsDownloaderService.selectedBsVersion$.next(props.version); }
|
||||
}
|
||||
|
||||
const openReleasePage = () => { ipcService.sendLazy("new-window", {args: props.version.ReleaseURL}); }
|
||||
const openReleasePage = () => { linkOpener.open(props.version.ReleaseURL) }
|
||||
|
||||
useEffect(() => {
|
||||
const sub = bsDownloaderService.selectedBsVersion$.pipe(distinctUntilChanged()).subscribe((version) => {
|
||||
@@ -38,25 +37,25 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {v
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="group m-3 relative w-72 h-60 transition-transform active:scale-[.98]" onClick={toggleSelect}>
|
||||
<div className={`absolute glow-on-hover group-hover:opacity-100 ${selected && "opacity-100"}`}></div>
|
||||
<div className={`relative flex flex-col overflow-hidden rounded-md w-72 h-60 cursor-pointer group-hover:shadow-none duration-300 bg-light-main-color-2 dark:bg-main-color-2 ${!selected && "shadow-lg shadow-gray-900"}`}>
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" loading="lazy"/>
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" loading="lazy"/>
|
||||
<div className="relative z-[1] p-2 w-full flex items-center justify-between grow">
|
||||
<div>
|
||||
<span className="block text-xl font-bold text-white tracking-wider">{props.version.BSVersion}</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-400">{formatedDate}</span>
|
||||
</div>
|
||||
{ props.version.ReleaseURL && (
|
||||
<a onClickCapture={e => { e.stopPropagation(); openReleasePage(); }} className="relative flex flex-row justify-between items-center rounded-full bg-black bg-opacity-30 text-white pb-[1px] hover:bg-opacity-50">
|
||||
<BsmIcon icon="steam" className="w-[25px] h-[25px] transition-transform group-hover:rotate-[-360deg] duration-300"/>
|
||||
<span className="relative -left-[2px] text-sm w-fit max-w-0 text-center overflow-hidden h-full whitespace-nowrap pb-[3px] transition-all group-hover:max-w-[200px] group-hover:px-1 duration-300">{t("pages.available-versions.steam-release")}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<li className="group relative w-72 h-60 transition-transform active:scale-[.98]" onClick={toggleSelect}>
|
||||
<span className={`absolute glow-on-hover group-hover:opacity-100 ${selected && "!opacity-100"}`}/>
|
||||
<div className={`relative flex flex-col overflow-hidden rounded-md w-72 h-60 cursor-pointer group-hover:shadow-none duration-300 bg-light-main-color-2 dark:bg-main-color-2 ${!selected && "shadow-lg shadow-gray-900"}`}>
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" loading="lazy"/>
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" loading="lazy"/>
|
||||
<div className="z-[1] p-2 w-full flex items-center justify-between grow">
|
||||
<div>
|
||||
<h2 className="block text-xl font-bold text-white tracking-wider">{props.version.BSVersion}</h2>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-400">{formatedDate}</span>
|
||||
</div>
|
||||
{ props.version.ReleaseURL && (
|
||||
<a onClickCapture={e => { e.stopPropagation(); openReleasePage(); }} className="flex flex-row justify-between items-center rounded-full bg-black bg-opacity-30 text-white pb-px hover:bg-opacity-50">
|
||||
<BsmIcon icon="steam" className="w-[25px] h-[25px] transition-transform group-hover:rotate-[-360deg] duration-300"/>
|
||||
<span className="relative -left-px text-sm w-fit max-w-0 text-center overflow-hidden h-full whitespace-nowrap pb-[3px] transition-all group-hover:max-w-[200px] group-hover:px-1 duration-300">{t("pages.available-versions.steam-release")}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -16,14 +16,15 @@ export function AvailableVersionsSlide(props: {year: string}) {
|
||||
});
|
||||
}, [])
|
||||
|
||||
//w-full max-w-full max-h-full flex items-start justify-center overflow-x-hidden overflow-y-scroll content-start scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-full max-h-full shrink-0 flex items-start justify-center overflow-x-hidden overflow-y-scroll content-start scrollbar-thin hover:scrollbar-thumb-neutral-900">
|
||||
<div className="relative left-[2px] flex justify-center items-start content-start flex-wrap max-w-6xl pb-4">
|
||||
{availableVersions.map((version, index) =>
|
||||
<AvailableVersionItem key={index} version={version}></AvailableVersionItem>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
//relative left-[2px] flex justify-center items-start content-start flex-wrap max-w-6xl
|
||||
|
||||
return (
|
||||
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap p-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
|
||||
{availableVersions.map((version, index) =>
|
||||
<AvailableVersionItem key={index} version={version}></AvailableVersionItem>
|
||||
)}
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
+10
-10
@@ -21,14 +21,14 @@ export function AvailableVersionsSlider() {
|
||||
});
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-fit max-h-full relative flex flex-col items-center grow min-h-0">
|
||||
<TabNavBar tabsText={availableYears} onTabChange={setSelectedYear}/>
|
||||
<div className="w-full min-h-0 flex transition-transform duration-300" style={{transform: `translate(${-(yearIndex * 100)}%, 0)`}}>
|
||||
{ availableYears.map((year, index) =>
|
||||
<AvailableVersionsSlide key={index} year={year}></AvailableVersionsSlide>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="w-full h-fit max-h-full flex flex-col items-center grow min-h-0">
|
||||
<TabNavBar className="mb-3" tabsText={availableYears} onTabChange={setSelectedYear}/>
|
||||
<ol className="w-full min-h-0 flex transition-transform duration-300" style={{transform: `translate(${-(yearIndex * 100)}%, 0)`}}>
|
||||
{ availableYears.map((year, index) =>
|
||||
<AvailableVersionsSlide key={index} year={year}></AvailableVersionsSlide>
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function EditVersionModal({resolver, clone = false}: {resolver: (x: Modal
|
||||
return (
|
||||
<form className="static" onSubmit={(e) => {e.preventDefault(); rename();}}>
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t(!clone ? "modals.edit-version.title" : "modals.clone-version.title")}</h1>
|
||||
<BsmIcon className="w-full h-11 my-3" icon="bsNote" style={{color: color}}/>
|
||||
<BsmIcon className="w-full h-11 my-3" icon="bsNote" style={{color}}/>
|
||||
{ clone && (
|
||||
<p className="max-w-sm mb-2 text-gray-800 dark:text-gray-200">{t("modals.clone-version.description")}</p>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ModalExitCode, ModalResponse, ModalService } from "../../../services/modale.service";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
|
||||
export function UninstallAllModsModal({resolver}: {resolver: (x: ModalResponse) => void}) {
|
||||
|
||||
const version = ModalService.getInsance().getModalData<BSVersion>();
|
||||
const t = useTranslation();
|
||||
|
||||
const desc = "modals.uninstall-all-mods.description";
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => {e.preventDefault(); resolver({exitCode: ModalExitCode.COMPLETED})}}>
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.uninstall-all-mods.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict}/>
|
||||
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t(desc, {version: version.name ?? version.BSVersion})}</p>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
|
||||
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => {resolver({exitCode: ModalExitCode.CANCELED})}} withBar={false} text="misc.cancel"/>
|
||||
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" type="submit" withBar={false} text="modals.bs-uninstall.buttons.submit"/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ModalExitCode, ModalResponse, ModalService } from "../../../services/modale.service";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
|
||||
|
||||
export function UninstallModModal({resolver}: {resolver: (x: ModalResponse) => void}) {
|
||||
|
||||
const mod = ModalService.getInsance().getModalData<Mod>();
|
||||
const t = useTranslation();
|
||||
|
||||
const desc = mod.name.toLowerCase() === "bsipa" ? "modals.uninstall-mod.description-bsipa" : "modals.uninstall-mod.description";
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => {e.preventDefault(); resolver({exitCode: ModalExitCode.COMPLETED})}}>
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.uninstall-mod.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict}/>
|
||||
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t(desc, {mod: mod.name})}</p>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
|
||||
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => {resolver({exitCode: ModalExitCode.CANCELED})}} withBar={false} text="misc.cancel"/>
|
||||
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" type="submit" withBar={false} text="modals.bs-uninstall.buttons.submit"/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { InstallationFolderModal } from "./modal-types/installation-folder-modal
|
||||
import { EditVersionModal } from "./modal-types/edit-version-modal.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { UninstallModModal } from "./modal-types/uninstall-mod-modal.component";
|
||||
import { UninstallAllModsModal } from "./modal-types/uninstall-all-mods-modal.component";
|
||||
|
||||
export function Modal() {
|
||||
|
||||
@@ -15,20 +17,28 @@ export function Modal() {
|
||||
const modalType = useObservable(modalSevice.modalType$);
|
||||
const {firstColor, secondColor} = useThemeColor();
|
||||
|
||||
const renderModal = () => {
|
||||
const resolver = modalSevice.getResolver();
|
||||
if(modalType === ModalType.STEAM_LOGIN){ return <LoginModal resolver={resolver}/> }
|
||||
if(modalType === ModalType.GUARD_CODE){ return <GuardModal resolver={resolver}/> }
|
||||
if(modalType === ModalType.UNINSTALL){ return <UninstallModal resolver={resolver}/> }
|
||||
if(modalType === ModalType.INSTALLATION_FOLDER){ return <InstallationFolderModal resolver={resolver}/> }
|
||||
if(modalType === ModalType.EDIT_VERSION){ return <EditVersionModal resolver={resolver}/> }
|
||||
if(modalType === ModalType.CLONE_VERSION){ return <EditVersionModal resolver={resolver}/> }
|
||||
if(modalType === ModalType.UNINSTALL_MOD){ return <UninstallModModal resolver={resolver}/> }
|
||||
if(modalType === ModalType.UNINSTALL_ALL_MODS){ return <UninstallAllModsModal resolver={resolver}/> }
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{modalType && (
|
||||
<div className="top-0 absolute w-screen h-screen flex content-center items-center justify-center z-50">
|
||||
<motion.span key="modal-overlay" onClick={() => modalSevice.resolve({exitCode: ModalExitCode.NO_CHOICE})} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{opacity: 0}} animate={{opacity: modalType && .60}} exit={{opacity: 0}} transition={{duration: .2}}/>
|
||||
<motion.div key="modal" initial={{y: "100vh"}} animate={{y: 0}} exit={{y: "100vh"}}>
|
||||
<div className="relative p-4 text-gray-200 overflow-hidden rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
|
||||
<div className="relative p-4 text-gray-800 dark:text-gray-200 overflow-hidden rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
|
||||
<span className="absolute top-0 w-full left-0 h-1" style={{backgroundImage: `linear-gradient(to right, ${firstColor}, ${secondColor})`}}/>
|
||||
{modalType === ModalType.STEAM_LOGIN && <LoginModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.GUARD_CODE && <GuardModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.UNINSTALL && <UninstallModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.INSTALLATION_FOLDER && <InstallationFolderModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.EDIT_VERSION && <EditVersionModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.CLONE_VERSION && <EditVersionModal resolver={modalSevice.getResolver()} clone/>}
|
||||
{renderModal()}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -67,9 +67,9 @@ export function BsVersionItem(props: {version: BSVersion}) {
|
||||
|
||||
|
||||
return (
|
||||
<div className={`outline-none relative p-[1px] overflow-hidden rounded-xl flex justify-center items-center mb-1 ${downloading && "nav-item-download"} active:translate-y-[1px]`}>
|
||||
<li className={`outline-none relative p-[1px] overflow-hidden rounded-xl flex justify-center items-center mb-1 ${downloading && "nav-item-download"} active:translate-y-[1px]`}>
|
||||
{downloading && <div className="download-progress absolute top-0 w-full h-full" style={{transform: `translate(${-(100 - downloadPercent)}%, 0)`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})`}}/>}
|
||||
<div className={`wrapper z-[1] px-1 py-[3px] w-full rounded-xl ${downloading && 'bg-black'} ${!downloading && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"} ${(isActive() && !downloading) && "bg-light-main-color-3 dark:bg-main-color-3"}`}>
|
||||
<div className={`wrapper z-[1] px-1 py-[3px] w-full rounded-xl ${downloading && 'bg-white dark:bg-black'} ${!downloading && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"} ${(isActive() && !downloading) && "bg-light-main-color-3 dark:bg-main-color-3"}`}>
|
||||
<Link onDoubleClick={handleDoubleClick} to={`/bs-version/${props.version.BSVersion}`} state={props.version} title={props.version.name && `${props.version.BSVersion} - ${props.version.name}`} className="w-full flex items-center justify-start content-center max-w-full">
|
||||
{props.version.steam && <BsmIcon icon="steam" className="w-[19px] h-[19px] mr-[5px] shrink-0"/>}
|
||||
{!props.version.steam && <BsmIcon icon="bsNote" className="w-[19px] h-[19px] mr-[5px] shrink-0" style={{color: props.version?.color ?? secondColor}}/>}
|
||||
@@ -79,7 +79,7 @@ export function BsVersionItem(props: {version: BSVersion}) {
|
||||
</Link>
|
||||
{downloading && <BsmButton onClick={cancel} className="my-1 text-xs text-white rounded-md text-center" withBar={false} text="misc.cancel" typeColor="error"/>}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -13,13 +13,11 @@ export function NavBar() {
|
||||
const installedVersions = useObservable(bsVersionServoce.installedVersions$);
|
||||
|
||||
return (
|
||||
<div id='nav-bar' className='z-10 flex flex-col h-full max-h-full items-center p-1 bg-light-main-color-1 dark:bg-main-color-1'>
|
||||
<div className='w-full flex items-start content-start justify-center relative mb-3'>
|
||||
<BsManagerIcon className='relative aspect-square w-16 h-16'/>
|
||||
</div>
|
||||
<div id='versions' className='w-fit max-w-[120px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-neutral-900 hover:overflow-y-scroll'>
|
||||
<nav id='nav-bar' className='z-10 flex flex-col h-full max-h-full items-center p-1 bg-light-main-color-1 dark:bg-main-color-1'>
|
||||
<BsManagerIcon className='relative aspect-square w-16 h-16 mb-3'/>
|
||||
<ol id='versions' className='w-fit max-w-[120px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 hover:overflow-y-scroll'>
|
||||
{installedVersions && installedVersions.map((version) => <BsVersionItem key={JSON.stringify(version)} version={version}/>)}
|
||||
</div>
|
||||
</ol>
|
||||
<div className='w-full p-2 flex flex-col items-center content-center justify-start'>
|
||||
<Link className='mb-2' to={"blah"}>
|
||||
<BsmIcon icon='add' className='text-blue-500 h-[34px]'/>
|
||||
@@ -28,6 +26,6 @@ export function NavBar() {
|
||||
<BsmIcon icon='settings' className='text-blue-500 h-7'/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,25 +12,25 @@ export function NotificationItem({resolver, notification}: {resolver?: (value: N
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const renderImage = () => {
|
||||
const renderImage = (() => {
|
||||
if(notification.type === NotificationType.SUCCESS){ return BeatRunningImg; }
|
||||
if(notification.type === NotificationType.WARNING){ return BeatWaitingImg; }
|
||||
if(notification.type === NotificationType.ERROR){ return BeatConflictImg; }
|
||||
return BeatImpatientImg;
|
||||
}
|
||||
})()
|
||||
|
||||
const renderNeonColors = () => {
|
||||
const renderNeonColors = (() => {
|
||||
if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
|
||||
if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
|
||||
if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
|
||||
return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<motion.div layout initial={{x: "110%"}} animate={{x: "0%"}} exit={{x:"110%"}} className="relative w-[270px] rounded-md overflow-hidden -left-[285px] mb-4 shadow-md shadow-black bg-gradient-to-br from-light-main-color-2 to-light-main-color-2 dark:from-main-color-2 dark:to-main-color-2 text-gray-800 dark:text-white">
|
||||
<motion.li layout initial={{x: "110%"}} animate={{x: "0%"}} exit={{x:"110%"}} className="relative w-[270px] rounded-md overflow-hidden -left-[285px] mb-4 shadow-md shadow-black bg-gradient-to-br from-light-main-color-2 to-light-main-color-2 dark:from-main-color-2 dark:to-main-color-2 text-gray-800 dark:text-white">
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="flex items-center justify-start pl-1">
|
||||
<BsmImage className="h-14 mr-1" image={renderImage()}/>
|
||||
<BsmImage className="h-14 mr-1" image={renderImage}/>
|
||||
<div className="flex flex-col py-2">
|
||||
<h1 className="font-bold leading-4 tracking-wide pr-5 my-1">{t(notification.title)}</h1>
|
||||
{notification.desc && <p className="text-xs pr-1">{t(notification.desc)}</p>}
|
||||
@@ -42,8 +42,8 @@ export function NotificationItem({resolver, notification}: {resolver?: (value: N
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className={`absolute w-full h-1 shadow-center left-0 top-0 ${renderNeonColors()}`}/>
|
||||
<span className={`absolute w-full h-1 shadow-center left-0 top-0 ${renderNeonColors}`}/>
|
||||
<BsmButton icon="cross" className="absolute top-2 right-2" withBar={false} onClick={() => resolver(NotificationResult.CLOSE)}/>
|
||||
</motion.div>
|
||||
</motion.li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook"
|
||||
import { NotificationService } from "renderer/services/notification.service"
|
||||
import { NotificationItem } from "./notification-item.component";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
|
||||
export function NotificationOverlay() {
|
||||
|
||||
@@ -11,10 +11,10 @@ export function NotificationOverlay() {
|
||||
|
||||
|
||||
return (
|
||||
<motion.ul className="absolute h-full w-0 top-0 right-0 z-40 bg-red-500 pt-10">
|
||||
<ul className="absolute h-full w-0 top-0 right-0 z-40 pt-10">
|
||||
<AnimatePresence>
|
||||
{notifications?.map(n => <NotificationItem key={n.id} resolver={n.resolver} notification={n.notification}/>)}
|
||||
</AnimatePresence>
|
||||
</motion.ul>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,27 +9,34 @@ export function BsmProgressBar() {
|
||||
|
||||
const progressBarService = ProgressBarService.getInstance();
|
||||
|
||||
const progress = useObservable(progressBarService.progression$);
|
||||
const progressData = useObservable(progressBarService.progressData$);
|
||||
const visible = useObservable(progressBarService.visible$);
|
||||
const style = useObservable(progressBarService.style$);
|
||||
|
||||
const progressLabel = (() => {
|
||||
if(!progressData){ return ""; }
|
||||
if(progressData?.label){ return progressData.label; }
|
||||
return `${Math.floor(progressData.progression)}%`
|
||||
})();
|
||||
const progressValue: number = progressData?.progression;
|
||||
|
||||
const {firstColor, secondColor} = useThemeColor();
|
||||
|
||||
return (
|
||||
<AnimatePresence> { visible &&
|
||||
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2">
|
||||
<div className={`flex items-center content-center justify-center bottom-9 z-10 rounded-lg bg-light-main-color-2 dark:bg-main-color-2 shadow-center shadow-black cursor-pointer transition-all duration-300 ${!progress && "h-14 w-14 rounded-full"} ${!!progress && "h-5 w-3/4 rounded-full p-[6px]"}`}>
|
||||
{ !!progress && (
|
||||
<>
|
||||
<div className="relative flex items-center h-full w-full rounded-full bg-black">
|
||||
<div className="w-0 h-full relative rounded-full download-progress flex items-center transition-all" style={{width: `${progress}%`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})`}}>
|
||||
<img className="h-[70px] w-[70px] min-w-fit absolute z-[1] -translate-y-1 -right-8 transition-all" src={BeatRunningImg} />
|
||||
</div>
|
||||
<span className="absolute w-full text-center text-white -top-[3px] left-0 text-[10px]">{`${Math.floor(progress)}%`}</span>
|
||||
return (
|
||||
<AnimatePresence> { visible &&
|
||||
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2 pointer-events-none" style={style}>
|
||||
<div className={`flex items-center content-center justify-center bottom-9 z-10 rounded-full bg-light-main-color-2 dark:bg-main-color-2 shadow-center shadow-black transition-all duration-300 ${!progressValue && "h-14 w-14 "} ${!!progressValue && "h-5 w-3/4 p-[6px]"}`}>
|
||||
{ !!progressValue && (
|
||||
<div className="relative h-full w-full rounded-full bg-black">
|
||||
<div className="w-0 h-full relative rounded-full download-progress flex items-center transition-all" style={{width: `${progressValue}%`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})`}}>
|
||||
<img className="h-[70px] w-[70px] min-w-fit absolute z-[1] -translate-y-1 -right-8" src={BeatRunningImg} />
|
||||
</div>
|
||||
<span className="absolute w-full text-center text-white -top-[3px] left-0 text-[10px]">{progressLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
{ !progressValue && <img className="w-12 h-12 spin-loading" src={BeatWaitingImg}></img> }
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{ !progress && <img className="w-12 h-12 spin-loading" src={BeatWaitingImg}></img> }
|
||||
</div>
|
||||
</motion.div>
|
||||
} </AnimatePresence>
|
||||
</motion.div>
|
||||
} </AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { CSSProperties } from "react";
|
||||
import { CSSProperties } from "react";
|
||||
import { Supporter } from "shared/models/supporters";
|
||||
import { motion } from "framer-motion";
|
||||
import txtBg from "../../../../../assets/images/gifs/txt-bg.gif"
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
|
||||
interface Props { supporter: Supporter, delay?: number }
|
||||
|
||||
export function SupporterItem({supporter, delay}: Props) {
|
||||
|
||||
const ipcService = IpcService.getInstance();
|
||||
const linkOpener = LinkOpenerService.getInstance();
|
||||
|
||||
const openSupporterLink = () => supporter.link && ipcService.sendLazy("new-window", {args: supporter.link});
|
||||
const openSupporterLink = () => supporter.link && linkOpener.open(supporter.link);
|
||||
|
||||
const additionnalStyles: CSSProperties = (() => {
|
||||
const commonStyles: CSSProperties = {backgroundImage: `url(${txtBg})`, backgroundRepeat: "no-repeat", backgroundPosition: "center"}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { Supporter } from "shared/models/supporters";
|
||||
import { SupporterItem } from "./supporter-item.component";
|
||||
|
||||
@@ -5,6 +6,8 @@ interface Props {className?: string, title: string, supporters: Supporter[]}
|
||||
|
||||
export function SupportersBlock({className, title, supporters}: Props) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const someDelay = () => {
|
||||
const [min, max] = [0, .55]
|
||||
const baseDelay = .15;
|
||||
@@ -13,7 +16,7 @@ export function SupportersBlock({className, title, supporters}: Props) {
|
||||
|
||||
return (
|
||||
<div className={`mb-10 flex flex-col items-center ${className}`}>
|
||||
<h2 className="uppercase text-3xl font-bold">{title}</h2>
|
||||
<h2 className="uppercase text-3xl font-bold">{t(title)}</h2>
|
||||
<div className="flex justify-center flex-wrap py-5 px-20">
|
||||
{supporters.map(s => <SupporterItem supporter={s} delay={someDelay()}/>)}
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,14 @@ import { SupportersService } from "renderer/services/supporters.service";
|
||||
import { SupportersBlock } from "./supporters-block.component";
|
||||
import ManheraChanGif from "../../../../../assets/images/gifs/menhera-chan.gif"
|
||||
import ManheraSadGif from "../../../../../assets/images/gifs/menhera-sad.gif"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
interface Props {isVisible: boolean, setVisible: (b: boolean) => void}
|
||||
|
||||
export function SupportersView({isVisible, setVisible}: Props) {
|
||||
|
||||
const supportersService = SupportersService.getInstance();
|
||||
const t = useTranslation();
|
||||
|
||||
const [supporters, setSupporters] = useState([] as Supporter[]);
|
||||
const [sponsors, setSponsors] = useState([] as Supporter[]);
|
||||
@@ -33,11 +35,11 @@ export function SupportersView({isVisible, setVisible}: Props) {
|
||||
<BsmButton className="absolute right-10 top-10 !bg-transparent w-7 h-7" icon="cross" withBar={false} onClick={() => setVisible(false)}></BsmButton>
|
||||
{(!!sponsors.length || !!supporters.length) && <img className="absolute bottom-5 right-5 rotate-45 w-32 h-32" src={ManheraChanGif}/>}
|
||||
<div className="w-full h-full overflow-y-scroll">
|
||||
{!!sponsors.length && <SupportersBlock className="mt-12" title="sponsors" supporters={sponsors}/>}
|
||||
{!!supporters.length && <SupportersBlock className="mt-12" title="supporters" supporters={supporters}/>}
|
||||
{!!sponsors.length && <SupportersBlock className="mt-12" title="pages.settings.patreon.view.sponsors" supporters={sponsors}/>}
|
||||
{!!supporters.length && <SupportersBlock className="mt-12" title="pages.settings.patreon.view.supporters" supporters={supporters}/>}
|
||||
{(!sponsors.length && !supporters.length) && (
|
||||
<>
|
||||
<h2 className="w-full text-center text-3xl font-bold mt-10 mb-24">No supporter yet</h2>
|
||||
<h2 className="w-full text-center text-3xl font-bold mt-10 mb-24">{t("pages.settings.patreon.view.no-supporters")}</h2>
|
||||
<img className="m-auto" src={ManheraSadGif} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {motion, useCycle} from "framer-motion"
|
||||
import { useState } from "react"
|
||||
|
||||
type Props = {className?: string, nbClickToShow?: number, children?: JSX.Element}
|
||||
|
||||
export function SpoilerClick({className, children, nbClickToShow = 5}: Props) {
|
||||
|
||||
const [remainingClicks, setRemainingClicks] = useState(nbClickToShow);
|
||||
const [translateY, setTranslateY] = useState("100%");
|
||||
|
||||
const handleClicks = () => {
|
||||
if(remainingClicks - 1 > 0){ setRemainingClicks(nb => nb - 1); }
|
||||
setTranslateY(`${(((remainingClicks - 1) / nbClickToShow) * 100)}%`)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div className={className} initial={{y: translateY}} animate={{y: translateY}} transition={{type: "spring", stiffness: 300, mass: 1 }} onClick={handleClicks}>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BsmIcon, BsmIconType } from "../svgs/bsm-icon.component"
|
||||
import OutsideClickHandler from 'react-outside-click-handler';
|
||||
import React from "react";
|
||||
import React, { MutableRefObject, Ref } from "react";
|
||||
import { BsmImage } from "./bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
|
||||
|
||||
type BsmButtonType = "primary"|"success"|"cancel"|"error";
|
||||
|
||||
@@ -22,29 +23,24 @@ type propsType = {
|
||||
onClickOutside?: (e: MouseEvent) => void,
|
||||
onClick?: (e: React.MouseEvent) => void,
|
||||
typeColor?:BsmButtonType,
|
||||
color?: string
|
||||
color?: string,
|
||||
title?: string,
|
||||
}
|
||||
|
||||
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color}: propsType) {
|
||||
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title}: propsType) {
|
||||
|
||||
const t = useTranslation();
|
||||
const secondColor = useThemeColor("second-color");
|
||||
|
||||
const primaryColor = typeColor === "primary" ? useThemeColor("first-color") : null;
|
||||
const primaryColor = typeColor === "primary" && useThemeColor("first-color");
|
||||
|
||||
const textColor = !primaryColor ? null : (() => {
|
||||
const hex = primaryColor.replaceAll("#", "");
|
||||
const uicolors = [parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255];
|
||||
const c = uicolors.map(col => {
|
||||
if (col <= 0.03928) { return col / 12.92; }
|
||||
return Math.pow((col + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
|
||||
return (L > 0.179) ? "#000" : "fff";
|
||||
const textColor = (() => {
|
||||
if(primaryColor){ return getCorrectTextColor(primaryColor); }
|
||||
return typeColor ? "white" : undefined;
|
||||
})();
|
||||
|
||||
const renderTypeColor = (() => {
|
||||
if(typeColor === "primary"){ return null; }
|
||||
if(typeColor === "primary"){ return ""; }
|
||||
if(!typeColor){ return `bg-light-main-color-2 dark:bg-main-color-2 ${!withBar && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"}`; }
|
||||
if(typeColor === "cancel"){ return "bg-gray-500"; }
|
||||
if(typeColor === "error"){ return "bg-red-500"; }
|
||||
@@ -54,10 +50,10 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
|
||||
|
||||
return (
|
||||
<OutsideClickHandler onOutsideClick={e => onClickOutside && onClickOutside(e)}>
|
||||
<div onClick={e => onClick && onClick(e)} className={`${className} overflow-hidden cursor-pointer group ${!withBar && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {backgroundColor: primaryColor ?? color})}}>
|
||||
<div onClick={onClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, backgroundColor: primaryColor || color}}>
|
||||
{ image && <BsmImage image={image} className={imgClassName}/> }
|
||||
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"}/> }
|
||||
{text && (type === "submit" ? <button className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: textColor})}} >{t(text)}</span>)}
|
||||
{text && (type === "submit" ? <button type="submit" className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: `${textColor}`})}}>{t(text)}</span>)}
|
||||
{ withBar && (
|
||||
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{color: secondColor}}>
|
||||
<div className="absolute top-0 left-0 h-full w-full bg-current brightness-50"/>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component"
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color"
|
||||
|
||||
type Props = {className?:string, checked?: boolean, onChange?: (val: boolean) => void, disabled?: boolean}
|
||||
|
||||
export function BsmCheckbox({className, checked, onChange, disabled} : Props) {
|
||||
|
||||
const checkedColor = useThemeColor("first-color");
|
||||
const iconColor = getCorrectTextColor(checkedColor);
|
||||
|
||||
const handleClick = () => {
|
||||
if(disabled){ return; }
|
||||
onChange && onChange(!checked);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className} group-one`}>
|
||||
{!disabled && <span className="glow-on-hover !w-[calc(100%+6px)] !h-[calc(100%+6px)] !-top-[3px] !-left-[3px] group-one-hover:opacity-100"/>}
|
||||
|
||||
<span className={`w-full h-full flex items-center justify-center rounded-md border-2 border-current overflow-hidden bg-light-main-color-3 dark:bg-main-color-1 ${disabled ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`} onClickCapture={e => {e.stopPropagation(); handleClick()}}>
|
||||
<BsmIcon className="w-full h-full" icon="check" style={{color: iconColor, backgroundColor: checkedColor, visibility: checked ? "visible" : "hidden"}}/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,18 +6,22 @@ import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
|
||||
export interface DropDownItem {text: string, icon?: BsmIconType, onClick?: () => void}
|
||||
|
||||
export function BsmDropdownButton({className, items, align}: {className?: string, items?: DropDownItem[], align?: "left"|"right"}) {
|
||||
type Props = {className?: string, items?: DropDownItem[], align?: "left"|"right", withBar?: boolean, icon?: BsmIconType, buttonClassName?: string, menuTranslationY?: string|number}
|
||||
|
||||
export function BsmDropdownButton({className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY}: Props) {
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const t = useTranslation()
|
||||
const t = useTranslation();
|
||||
|
||||
const defaultButtonClassName = "relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black"
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<BsmButton onClick={() => setExpanded(!expanded)} className='relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black' icon="settings" active={expanded} onClickOutside={() => {setExpanded(false)}}/>
|
||||
<div className={`pt-1 pb-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md overflow-hidden bg-light-main-color-2 dark:bg-main-color-2 text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-transform ease-in-out ${align === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"}`} style={{transform: expanded ? "scale(1)" : "scale(0)"}}>
|
||||
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} onClickOutside={() => {setExpanded(false)}} withBar={withBar}/>
|
||||
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] ease-in-out ${align === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"}`} style={{scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}`}}>
|
||||
{ items?.map((i, index) => !!i &&(
|
||||
<div key={index} onClick={!!i.onClick ? i.onClick : undefined} className="flex justify-start items-center w-full pr-3 pl-3 pt-2 pb-2 hover:bg-light-main-color-3 dark:hover:bg-main-color-3">
|
||||
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-gray-800 dark:text-white"></BsmIcon>}
|
||||
<div key={index} onClick={!!i.onClick ? i.onClick : undefined} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
|
||||
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-inherit"></BsmIcon>}
|
||||
<span className="w-max">{t(i.text)}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export function BsmImage({className, image, errorImage, placeholder, loading}: {className?: string, image: string, errorImage?: string, placeholder?: string, loading?: "lazy"|"eager"}) {
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function BsmImage({className, image, errorImage, placeholder, loading, style}: {className?: string, image: string, errorImage?: string, placeholder?: string, loading?: "lazy"|"eager", style?: CSSProperties}) {
|
||||
|
||||
return (
|
||||
<img className={className} src={image} loading={loading} onError={({currentTarget}) => { currentTarget.onerror = null; currentTarget.src = errorImage ? errorImage : "" }} style={{backgroundImage: `url(${placeholder})`, backgroundSize: "cover"}}/>
|
||||
<img className={`${className} pointer-events-none`} src={image} loading={loading} onError={({currentTarget}) => { currentTarget.onerror = null; currentTarget.src = errorImage || "" }} style={{...style, backgroundImage: `url(${placeholder})`, backgroundSize: "cover"}} alt=""/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,16 +15,16 @@ export function TabNavBar(props: {tabsText: string[], onTabChange: Function, cla
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative h-8 shrink-0 cursor-pointer rounded-md overflow-hidden mb-3 shadow-md shadow-black ${props.className}`}>
|
||||
<nav className={`relative h-8 shrink-0 cursor-pointer rounded-md overflow-hidden shadow-md shadow-black ${props.className}`}>
|
||||
<div className="absolute w-full h-1 bottom-0" style={{color: secondColor}}>
|
||||
<span className="absolute h-full w-full bg-current brightness-50" />
|
||||
<span className="absolute h-full block bg-current transition-transform duration-300 shadow-center shadow-current" style={{transform: `translate(${currentTabIndex * 100}%, 0)`, width: `calc(100% / ${props.tabsText.length})`}}/>
|
||||
</div>
|
||||
<div className="grid" style={{gridTemplateColumns: `repeat(${props.tabsText.length}, minmax(0, 1fr))`}}>
|
||||
<ul className="grid" style={{gridTemplateColumns: `repeat(${props.tabsText.length}, minmax(0, 1fr))`}}>
|
||||
{ props.tabsText.map((y, index) =>
|
||||
<span className="pr-4 pl-4 h-full inline-block bg-light-main-color-2 text-gray-800 dark:bg-main-color-2 dark:text-gray-200 text-lg font-bold text-center hover:bg-light-main-color-1 dark:hover:bg-main-color-1" key={index} onClick={() => selectYear(y)}>{t(y)}</span>
|
||||
<li className="px-4 h-full text-center bg-light-main-color-2 text-gray-800 dark:bg-main-color-2 dark:text-gray-200 text-lg font-bold hover:bg-light-main-color-1 dark:hover:bg-main-color-1" key={index} onClick={() => selectYear(y)}>{t(y)}</li>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BsmImage } from '../shared/bsm-image.component';
|
||||
import './slideshow.component.css';
|
||||
|
||||
export function Slideshow(props: {className: string, images: string[]}) {
|
||||
return (
|
||||
<div className={`slide ${props.className}`}>
|
||||
{
|
||||
props.images.map((i, index) => <img key={index} className='w-full h-full object-cover' src={i} style={{animationDelay: `${index * 10}s`}}></img>)
|
||||
props.images.map((i, index) => <BsmImage key={i} className='w-full h-full object-cover' image={i} style={{animationDelay: `${index * 10}s`}}/>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -19,11 +19,16 @@ import { CSSProperties, memo } from "react";
|
||||
import EditIcon from "./icons/edit-icon.component";
|
||||
import { ExportIcon } from "./icons/export-icon.component";
|
||||
import PatreonIcon from "./icons/patreon-icon.component";
|
||||
import { SearchIcon } from "./icons/search-icon.component";
|
||||
import { CheckIcon } from "./icons/check-icon.component";
|
||||
import { ThreeDotsIcon } from "./icons/three-dots-icon.component";
|
||||
import { GitHubIcon } from "./icons/github-icon.component";
|
||||
import { CloseIcon } from "./icons/close-icon.component";
|
||||
|
||||
export type BsmIconType = (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
|
||||
);
|
||||
|
||||
@@ -50,6 +55,11 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
|
||||
if(icon === "edit"){ return <EditIcon className={className} style={style}/> }
|
||||
if(icon === "export"){ return <ExportIcon className={className} style={style}/> }
|
||||
if(icon === "patreon"){ return <PatreonIcon className={className} style={style}/> }
|
||||
if(icon === "search"){ return <SearchIcon className={className} style={style}/> }
|
||||
if(icon === "check"){ return <CheckIcon className={className} style={style}/> }
|
||||
if(icon === "three-dots"){ return <ThreeDotsIcon className={className} style={style}/> }
|
||||
if(icon === "github"){ return <GitHubIcon className={className} style={style}/> }
|
||||
if(icon === "close"){ return <CloseIcon className={className} style={style}/> }
|
||||
return <TrashIcon className={className} style={style}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function CheckIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40">
|
||||
<path fill="currentColor" d="M15.792 29.458q-.292 0-.563-.104-.271-.104-.521-.354l-7.416-7.417q-.459-.458-.459-1.145 0-.688.459-1.146.458-.459 1.104-.459.646 0 1.146.459l6.25 6.291 14.666-14.666q.459-.459 1.125-.459.667 0 1.125.459.459.458.459 1.145 0 .688-.459 1.146L16.917 29q-.25.25-.521.354-.271.104-.604.104Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from 'react'
|
||||
|
||||
export function CloseIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40">
|
||||
<path fill="currentColor" d="m20 22.208-8.417 8.417q-.458.458-1.104.458-.646 0-1.104-.458-.458-.458-.458-1.104 0-.646.458-1.104L17.792 20l-8.417-8.417q-.458-.458-.458-1.104 0-.646.458-1.104.458-.458 1.104-.458.646 0 1.104.458L20 17.792l8.417-8.417q.458-.458 1.104-.458.646 0 1.104.458.458.458.458 1.104 0 .646-.458 1.104L22.208 20l8.417 8.417q.458.458.458 1.104 0 .646-.458 1.104-.458.458-1.104.458-.646 0-1.104-.458Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from 'react'
|
||||
|
||||
export function GitHubIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function SearchIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40">
|
||||
<path fill="currentColor" d="m31.833 34.167-9.708-9.75q-1.25 1.041-2.896 1.625-1.646.583-3.521.583-4.583 0-7.77-3.208-3.188-3.209-3.188-7.709 0-4.541 3.188-7.729 3.187-3.187 7.729-3.187 4.541 0 7.729 3.187 3.187 3.188 3.187 7.729 0 1.792-.583 3.438-.583 1.646-1.625 3.021l9.792 9.75q.458.458.458 1.104 0 .646-.5 1.146-.458.458-1.146.458-.687 0-1.146-.458ZM15.667 23.458q3.25 0 5.5-2.27 2.25-2.271 2.25-5.48 0-3.25-2.25-5.5t-5.5-2.25q-3.209 0-5.479 2.25-2.271 2.25-2.271 5.5 0 3.209 2.25 5.48 2.25 2.27 5.5 2.27Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function ThreeDotsIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
|
||||
<path fill="currentColor" d="M12.025 21.15q-1 0-1.7-.7t-.7-1.675q0-1 .7-1.7t1.675-.7q1 0 1.688.7.687.7.687 1.7 0 .975-.687 1.675-.688.7-1.663.7Zm0-6.775q-1 0-1.7-.7T9.625 12q0-1 .7-1.688.7-.687 1.675-.687 1 0 1.688.687.687.688.687 1.663 0 1-.687 1.7-.688.7-1.663.7Zm0-6.75q-1 0-1.7-.713-.7-.712-.7-1.687 0-1 .7-1.688.7-.687 1.675-.687 1 0 1.688.687.687.688.687 1.688 0 .975-.687 1.687-.688.713-1.663.713Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export default function TitleBar({template = "main"} : {template?: "update"|"mai
|
||||
}
|
||||
|
||||
return (
|
||||
<header id="titlebar" className="min-h-[22px] bg-light-main-color-1 dark:bg-main-color-1 w-screen h-[22px] flex content-center items-center justify-start z-10">
|
||||
<header id="titlebar" className="min-h-[22px] bg-light-main-color-1 shrink-0 dark:bg-main-color-1 w-screen h-[22px] flex content-center items-center justify-start z-10">
|
||||
<div id="drag-region" className='grow basis-0 h-full'>
|
||||
{template !== "update" && (<div id="window-title" className='pl-1'>
|
||||
<span className='text-gray-800 dark:text-gray-100 font-bold text-xs italic'>BSManager</span>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BsmIcon, BsmIconType } from "renderer/components/svgs/bsm-icon.component"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
type props = {onClick: (active: boolean) => void, active: boolean, text: string, icon: BsmIconType}
|
||||
|
||||
export function LaunchModToogle({onClick, active, text, icon}: props) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!active && "shadow-md shadow-black"}`} onClick={() => onClick(!active)}>
|
||||
<div className={`absolute glow-on-hover !rounded-full ${active && "!opacity-100 blur-[2px]"}`}/>
|
||||
<div className='w-full h-full px-6 flex justify-center items-center bg-light-main-color-2 dark:bg-main-color-2 p-3 rounded-full text-gray-800 dark:text-white group-hover:bg-light-main-color-1 dark:group-hover:bg-main-color-1'>
|
||||
<BsmIcon icon={icon} className='mr-1 h-7 text-gray-800 dark:text-white'/>
|
||||
<span className='w-fit min-w-fit h-full text-lg font-bold uppercase tracking-wide italic'>{t(text)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { LaunchModToogle } from "./launch-mod-toogle.component";
|
||||
|
||||
type props = {version: BSVersion};
|
||||
|
||||
export function LaunchSlide({version}: props) {
|
||||
|
||||
const configService = ConfigurationService.getInstance();
|
||||
const bsLauncherService = BSLauncherService.getInstance();
|
||||
|
||||
const [oculusMode, setOculusMode] = useState(!!configService.get<boolean>(LaunchMods.OCULUS_MOD));
|
||||
const [desktopMode, setDesktopMode] = useState(!!configService.get<boolean>(LaunchMods.DESKTOP_MOD));
|
||||
const [debugMode, setDebugMode] = useState(!!configService.get<boolean>(LaunchMods.DEBUG_MOD));
|
||||
|
||||
const launchState = useObservable(bsLauncherService.launchState$);
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
if(mode === LaunchMods.DEBUG_MOD){ setDebugMode(value); }
|
||||
else if(mode === LaunchMods.OCULUS_MOD){
|
||||
setOculusMode(value);
|
||||
setDesktopMode(false);
|
||||
configService.set(LaunchMods.DESKTOP_MOD, false);
|
||||
}
|
||||
else if(mode === LaunchMods.DESKTOP_MOD){
|
||||
setDesktopMode(value);
|
||||
setOculusMode(false);
|
||||
configService.set(LaunchMods.OCULUS_MOD, false);
|
||||
}
|
||||
configService.set(mode, value);
|
||||
}
|
||||
|
||||
const launch = () => bsLauncherService.launch(version, oculusMode, desktopMode, debugMode)
|
||||
|
||||
return (
|
||||
<div className="w-full shrink-0 items-center relative flex flex-col justify-start">
|
||||
<div className='grid grid-flow-col grid-cols-3 gap-6'>
|
||||
<LaunchModToogle icon='oculus' onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus"/>
|
||||
<LaunchModToogle icon='desktop' onClick={() => setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/>
|
||||
<LaunchModToogle icon='terminal' onClick={() => setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug"/>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(launchState)} className='relative text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { CSSProperties, useRef} from "react"
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { PageStateService } from "renderer/services/page-state.service";
|
||||
import useDoubleClick from 'use-double-click';
|
||||
|
||||
type Props = {className?: string, mod: Mod, installedVersion: string, isDependency?: boolean, isSelected?: boolean, onChange?: (val: boolean) => void, wantInfo?: boolean, onWantInfo?: (mod: Mod) => void}
|
||||
|
||||
export function ModItem({className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo}: Props) {
|
||||
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
const pageState = PageStateService.getInstance();
|
||||
|
||||
const themeColor = useThemeColor("second-color");
|
||||
const uninstalling = useObservable(modsManager.isUninstalling$);
|
||||
const clickRef = useRef();
|
||||
|
||||
useDoubleClick({
|
||||
onSingleClick: e => handleWantInfo(e),
|
||||
onDoubleClick: e => handleOnChange(e),
|
||||
ref: clickRef,
|
||||
latency: 175
|
||||
})
|
||||
|
||||
const wantInfoStyle: CSSProperties = wantInfo ? {borderColor: themeColor} : {borderColor: "transparent"};
|
||||
const isOutDated = installedVersion < mod.version;
|
||||
|
||||
const uninstall = () => {
|
||||
modsManager.uninstallMod(mod, pageState.getState());
|
||||
}
|
||||
|
||||
const handleWantInfo = (e: React.MouseEvent<Element, MouseEvent>) => { e.preventDefault(); onWantInfo(mod); }
|
||||
const handleOnChange = (e: React.MouseEvent<Element, MouseEvent>) => { e.preventDefault(); onChange(!isChecked); }
|
||||
|
||||
const isChecked = isDependency || isSelected || mod.required;
|
||||
|
||||
return (
|
||||
<li ref={clickRef} className={`${className} group`}>
|
||||
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={onChange} disabled={mod.required || isDependency} checked={isChecked}/>
|
||||
</div>
|
||||
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>{mod.name}</span>
|
||||
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${(installedVersion && isOutDated) && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>{installedVersion || "-"}</span>
|
||||
<span className="min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>{mod.version}</span>
|
||||
<span title={mod.description} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>{mod.description}</span>
|
||||
<div className="h-full bg-inherit flex items-center justify-center mr-3 rounded-r-md pr-2 border-t-2 border-b-2 border-r-2 group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{installedVersion && <BsmButton className="z-[1] h-7 w-7 p-[5px] rounded-full group-hover:brightness-90" icon="trash" disabled={uninstalling} withBar={false} onClick={e => {e.stopPropagation(); uninstall()}}/>}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { PageStateService } from "renderer/services/page-state.service";
|
||||
import { Mod } from "shared/models/mods/mod.interface"
|
||||
import { ModItem } from "./mod-item.component"
|
||||
|
||||
type Props = {modsMap: Map<string, Mod[]>, installed: Map<string, Mod[]>, modsSelected: Mod[], onModChange: (selected: boolean, mod: Mod) => void, moreInfoMod?: Mod, onWantInfos: (mod: Mod) => void}
|
||||
|
||||
export function ModsGrid({modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos}: Props) {
|
||||
|
||||
const pageState = PageStateService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
const [filter, setFilter] = useState("");
|
||||
const [filterEnabled, setFilterEnabled] = useState(false);
|
||||
const t = useTranslation();
|
||||
|
||||
const installedModVersion = (key: string, mod: Mod): string => {
|
||||
if(!installed || !installed.get(key)){ return undefined; }
|
||||
const installedMod = installed.get(key).find(m => m.name === mod.name);
|
||||
if(!installedMod){ return undefined }
|
||||
return installedMod.version
|
||||
}
|
||||
|
||||
const isDependency = (mod: Mod): boolean => {
|
||||
return modsSelected.some(m => {
|
||||
const deps = m.dependencies.map(dep => Array.from(modsMap.values()).flat().find(m => dep.name === m.name));
|
||||
if(deps.some(depMod => depMod.name === mod.name)){ return true; }
|
||||
return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
|
||||
});
|
||||
}
|
||||
|
||||
const isSelected = (mod: Mod): boolean => modsSelected.some(m => m.name === mod.name);
|
||||
|
||||
const handleInput = (val: string) => setFilter(val.toLowerCase());
|
||||
|
||||
const handleToogleFilter = () => {
|
||||
setFilter("");
|
||||
setFilterEnabled(b => !b);
|
||||
}
|
||||
|
||||
const handleUninstallAll = () => {
|
||||
modsManager.uninstallAllMods(pageState.getState())
|
||||
}
|
||||
|
||||
return modsMap && (
|
||||
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
|
||||
<span className="absolute z-10 top-0 w-full h-8 bg-inherit"/>
|
||||
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1">
|
||||
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter}/>
|
||||
</span>
|
||||
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1">
|
||||
{(filterEnabled ? (
|
||||
<motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{width: 0}} animate={{width: "250px"}} transition={{ease:"easeInOut", duration:.15}} onChange={e => handleInput(e.target.value)}/>
|
||||
):(
|
||||
<span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>
|
||||
))}
|
||||
</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">{t("pages.version-viewer.mods.mods-grid.header-bar.installed")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
|
||||
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
|
||||
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[
|
||||
({text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: handleUninstallAll}),
|
||||
]}/>
|
||||
</span>
|
||||
|
||||
{
|
||||
Array.from(modsMap.keys()).map(key => modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
|
||||
<ul key={key} className="contents">
|
||||
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
|
||||
{modsMap.get(key).map(mod => mod.name.toLowerCase().includes(filter) && (
|
||||
<ModItem key={mod.name} className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer" mod={mod} installedVersion={installedModVersion(key, mod)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={(val) => onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.name === moreInfoMod?.name}/>
|
||||
))}
|
||||
</ul>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { ModsGrid } from "./mods-grid.component";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { DefaultConfigKey } from "renderer/config/default-configuration.config";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png"
|
||||
import { SpoilerClick } from "renderer/components/shared/UwU/spoiler-click.component";
|
||||
import YuruYuriDance from "../../../../../../assets/images/gifs/yuruyuri-dance.gif"
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { skip, filter } from "rxjs/operators";
|
||||
import { Subscription } from "rxjs";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
|
||||
export function ModsSlide({version}: {version: BSVersion}) {
|
||||
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
const configService = ConfigurationService.getInstance();
|
||||
const linkOpener = LinkOpenerService.getInstance();
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [modsAvailable, setModsAvailable] = useState(null as Map<string, Mod[]>);
|
||||
const [modsInstalled, setModsInstalled] = useState(null as Map<string, Mod[]>);
|
||||
const [modsSelected, setModsSelected] = useState([] as Mod[]);
|
||||
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
|
||||
const installing = useObservable(modsManager.isInstalling$);
|
||||
const t = useTranslation();
|
||||
|
||||
const downloadRef = useRef(null);
|
||||
const [downloadWith, setDownloadWidth] = useState(0);
|
||||
|
||||
const modsToCategoryMap = (mods: Mod[]): Map<string, Mod[]> => {
|
||||
if(!mods){ return new Map<string, Mod[]>(); }
|
||||
const map = new Map<string, Mod[]>();
|
||||
mods.forEach(mod => map.set(mod.category, [...map.get(mod.category) ?? [], mod]));
|
||||
return map;
|
||||
}
|
||||
|
||||
const handleModChange = (selected: boolean, mod: Mod) => {
|
||||
if(selected){ return setModsSelected([...modsSelected, mod]); }
|
||||
const mods = [...modsSelected];
|
||||
mods.splice(mods.findIndex(m => m.name === mod.name), 1);
|
||||
setModsSelected(mods);
|
||||
}
|
||||
|
||||
const handleMoreInfo = (mod: Mod) => {
|
||||
if(mod.name === moreInfoMod?.name){ return setMoreInfoMod(null); }
|
||||
setMoreInfoMod(mod);
|
||||
}
|
||||
|
||||
const handleOpenMoreInfo = () => {
|
||||
if(!moreInfoMod || !moreInfoMod.link){ return; }
|
||||
linkOpener.open(moreInfoMod.link);
|
||||
}
|
||||
|
||||
const installMods = () => {
|
||||
if(installing){ return; }
|
||||
modsManager.installMods(modsSelected, version).then(() => {
|
||||
loadMods();
|
||||
});
|
||||
}
|
||||
|
||||
const loadMods = () => {
|
||||
Promise.all([
|
||||
modsManager.getAvailableMods(version),
|
||||
modsManager.getInstalledMods(version)
|
||||
]).then(([available, installed]) => {
|
||||
const defaultMods = configService.get<string[]>("default_mods" as DefaultConfigKey);
|
||||
setModsAvailable(modsToCategoryMap(available));
|
||||
setModsSelected(available.filter(m => m.required || defaultMods.some(d => m.name.toLowerCase() === d.toLowerCase()) || installed.some(i => m.name === i.name)));
|
||||
setModsInstalled(modsToCategoryMap(installed))
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
if(isVisible){
|
||||
loadMods();
|
||||
subs.push(modsManager.isUninstalling$.pipe(skip(1), filter(uninstalling => !uninstalling)).subscribe(() => {
|
||||
loadMods();
|
||||
}))
|
||||
}
|
||||
|
||||
return () => {
|
||||
setMoreInfoMod(null);
|
||||
setModsAvailable(null);
|
||||
setModsInstalled(null);
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
}, [isVisible, version]);
|
||||
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if(modsAvailable){
|
||||
setDownloadWidth(downloadRef?.current?.offsetWidth)
|
||||
}
|
||||
}, [modsAvailable])
|
||||
|
||||
|
||||
return (
|
||||
<VisibilitySensor onChange={setIsVisible}>
|
||||
<div className='shrink-0 w-full h-full px-8 pb-7 flex justify-center'>
|
||||
<div className='relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 h-full w-full rounded-md shadow-black shadow-center overflow-hidden'>
|
||||
{modsAvailable ? (
|
||||
<>
|
||||
<div className="overflow-scroll w-full min-h-0 scrollbar-thin scrollbar-thumb-neutral-900 scrollbar-thumb-rounded-full">
|
||||
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo}/>
|
||||
</div>
|
||||
<div className="h-10 shrink-0 flex items-center justify-between px-3">
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{width: downloadWith}}/>
|
||||
<div ref={downloadRef}>
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.install-or-update" withBar={false} disabled={installing} typeColor="primary" onClick={installMods}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center relative">
|
||||
<img className="w-32 h-32 spin-loading" src={BeatWaitingImg}></img>
|
||||
<span className="text-xl mt-3 h-0 italic">{t("pages.version-viewer.mods.loading-mods")}</span>
|
||||
<SpoilerClick className="absolute right-5 w-20 h-[120px] bottom-5 cursor-pointer">
|
||||
<img className="relative w-full" src={YuruYuriDance}/>
|
||||
</SpoilerClick>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</VisibilitySensor>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,9 @@ export const defaultConfiguration: {[key in DefaultConfigKey]: any} = {
|
||||
"theme": "os",
|
||||
"language": window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
|
||||
"supported_languages": ["en-US", "en-EN", "fr-FR", "es-ES"],
|
||||
"default_mods": ["SongCore", "ScoreSaber", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager", "ModelDownloader"]
|
||||
}
|
||||
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages";
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods";
|
||||
|
||||
export type ThemeConfig = "dark" | "light" | "os"
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Return text color for best constrast with background color
|
||||
* @param {string} bgHex
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getCorrectTextColor(bgHex: string): string{
|
||||
const hex = bgHex.replaceAll("#", "");
|
||||
const uicolors = [parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255];
|
||||
const c = uicolors.map(col => {
|
||||
if (col <= 0.03928) { return col / 12.92; }
|
||||
return (col + 0.055) / 1.055 ** 2.4;
|
||||
});
|
||||
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
|
||||
return (L > 0.128) ? "#000" : "fff";
|
||||
}
|
||||
+22
-4
@@ -22,10 +22,6 @@
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.download-progress{
|
||||
@@ -37,6 +33,28 @@
|
||||
animation: spin-loading 2s cubic-bezier(.46,-0.55,.49,1.52) 0s forwards infinite;
|
||||
}
|
||||
|
||||
.glow-on-hover {
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(45deg, #ff0000, #ff7300, #fffb00, #48ff00, #00ffd5, #002bff, #7a00ff, #ff00c8, #ff0000);
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left:-4px;
|
||||
background-size: 400%;
|
||||
z-index: -1;
|
||||
filter: blur(3px);
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
animation: glowing 20s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes glowing {
|
||||
0% { background-position: 0 0; }
|
||||
50% { background-position: 400% 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
|
||||
@keyframes spin-loading{
|
||||
0%{ transform: rotate(-360deg); }
|
||||
50%{ transform: rotate(-360deg); }
|
||||
|
||||
@@ -17,6 +17,8 @@ import { NotificationService } from "renderer/services/notification.service";
|
||||
import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
import { ThemeService } from "renderer/services/theme.service";
|
||||
import { SupportersView } from "renderer/components/settings/supporters-view/supporters-view.component";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function SettingsPage() {
|
||||
|
||||
@@ -29,6 +31,7 @@ export function SettingsPage() {
|
||||
const authService: AuthUserService = AuthUserService.getInstance();
|
||||
const notificationService: NotificationService = NotificationService.getInstance();
|
||||
const i18nService: I18nService = I18nService.getInstance();
|
||||
const linkOpener: LinkOpenerService = LinkOpenerService.getInstance();
|
||||
|
||||
const {firstColor, secondColor} = useThemeColor();
|
||||
const sessionExist = useObservable(authService.sessionExist$);
|
||||
@@ -48,6 +51,7 @@ export function SettingsPage() {
|
||||
const [installationFolder, setInstallationFolder] = useState(null);
|
||||
const [showSupporters, setShowSupporters] = useState(false);
|
||||
const [appVersion, setAppVersion] = useState("");
|
||||
const nav = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
loadInstallationFolder();
|
||||
@@ -111,17 +115,29 @@ export function SettingsPage() {
|
||||
};
|
||||
|
||||
const openPatreonPage = () => {
|
||||
ipcService.sendLazy("new-window", {args: "https://www.patreon.com/bsmanager?fan_landing=true"})
|
||||
linkOpener.open("https://www.patreon.com/bsmanager?fan_landing=true");
|
||||
}
|
||||
|
||||
const toogleShowSupporters = () => {
|
||||
setShowSupporters(show => !show);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center overflow-y-scroll scrollbar-thin scrollbar-thumb-neutral-900 text-gray-800 dark:text-gray-200">
|
||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||
const openReportBug = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&template=bug_report.md&title=%5BBUG%5D");
|
||||
const openRequestFeatures = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=enhancement&template=feature_request.md&title=%5BAME%5D");
|
||||
|
||||
<div className="max-w-2xl w-full mt-10">
|
||||
const openLogs = () => ipcService.sendLazy("open-logs");
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 text-gray-800 dark:text-gray-200">
|
||||
|
||||
|
||||
|
||||
<div className="max-w-2xl w-full">
|
||||
|
||||
<div className="inline-block sticky top-8 left-[calc(100%)] translate-x-12 grow-0 w-9 h-9">
|
||||
<BsmButton className="inline-block grow-0 bg-transparent sticky h-full w-full top-20 right-20 !m-0 rounded-full p-1" onClick={() => nav(-1)} icon="close" withBar={false}/>
|
||||
</div>
|
||||
|
||||
<SettingContainer title="pages.settings.steam.title" description="pages.settings.steam.description">
|
||||
<BsmButton onClick={deleteSteamSession} className="w-fit px-3 py-[2px] text-white rounded-md" withBar={false} text="pages.settings.steam.logout" typeColor="error" disabled={!sessionExist}/>
|
||||
@@ -156,6 +172,18 @@ export function SettingsPage() {
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 whitespace-nowrap mr-2 !text-white" iconClassName="mr-1" text="pages.settings.patreon.buttons.support" icon="patreon" color="#EC6350" withBar={false} onClick={openPatreonPage}/>
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="pages.settings.patreon.buttons.supporters" color="#6c5ce7" onClick={toogleShowSupporters}/>
|
||||
</div>
|
||||
<SettingContainer className="pt-3" description="pages.settings.contribution.description">
|
||||
<div className="flex items-center justify-between w-full h-8 bg-light-main-color-1 dark:bg-main-color-1 rounded-md pl-2 py-1">
|
||||
<div className="flex">
|
||||
<BsmButton onClick={openRequestFeatures} className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" text="pages.settings.contribution.buttons.request-features" withBar={false}/>
|
||||
<BsmButton onClick={openReportBug} className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" text="pages.settings.contribution.buttons.report-bug" withBar={false}/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<BsmButton onClick={openLogs} className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" text="pages.settings.contribution.buttons.open-logs" withBar={false}/>
|
||||
<BsmButton onClick={openGithub} className="h-full shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" icon="github" title="GitHub" withBar={false}/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingContainer>
|
||||
</SettingContainer>
|
||||
|
||||
<span className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md py-1 px-2 font-bold float-right">v{appVersion}</span>
|
||||
|
||||
@@ -1,100 +1,56 @@
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import BSLogo from '../../../assets/images/apngs/bs-logo.png';
|
||||
import { BSLauncherService, LaunchMods } from '../services/bs-launcher.service';
|
||||
import { TabNavBar } from 'renderer/components/shared/tab-nav-bar.component';
|
||||
import wipGif from "../../../assets/images/gifs/wip.gif"
|
||||
import { ConfigurationService } from 'renderer/services/configuration.service';
|
||||
import { BsmDropdownButton } from 'renderer/components/shared/bsm-dropdown-button.component';
|
||||
import { BsmIcon, BsmIconType } from 'renderer/components/svgs/bsm-icon.component';
|
||||
import { BsmImage } from 'renderer/components/shared/bsm-image.component';
|
||||
import { BsmButton } from 'renderer/components/shared/bsm-button.component';
|
||||
import { BSUninstallerService } from '../services/bs-uninstaller.service';
|
||||
import { BSVersionManagerService } from '../services/bs-version-manager.service';
|
||||
import { ModalExitCode, ModalService, ModalType } from '../services/modale.service';
|
||||
import DefautVersionImage from "../../../assets/images/default-version-img.jpg";
|
||||
import { BsDownloaderService } from 'renderer/services/bs-downloader.service';
|
||||
import { useTranslation } from 'renderer/hooks/use-translation.hook';
|
||||
import { IpcService } from 'renderer/services/ipc.service';
|
||||
import { useObservable } from 'renderer/hooks/use-observable.hook';
|
||||
import { MapService } from 'renderer/services/maps.service';
|
||||
import { LaunchSlide } from 'renderer/components/version-viewer/slides/launch/launch-slide.component';
|
||||
import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-slide.component';
|
||||
|
||||
export function VersionViewer() {
|
||||
|
||||
const configService = ConfigurationService.getInstance();
|
||||
const bsUninstallerService = BSUninstallerService.getInstance();
|
||||
const bsVersionManagerService = BSVersionManagerService.getInstance();
|
||||
const modalService = ModalService.getInsance();
|
||||
const bsDownloaderService = BsDownloaderService.getInstance();
|
||||
const ipcService = IpcService.getInstance();
|
||||
const mapService = MapService.getInstance();
|
||||
const bsUninstallerService = BSUninstallerService.getInstance();
|
||||
const bsVersionManagerService = BSVersionManagerService.getInstance();
|
||||
const modalService = ModalService.getInsance();
|
||||
const bsDownloaderService = BsDownloaderService.getInstance();
|
||||
const ipcService = IpcService.getInstance();
|
||||
const mapService = MapService.getInstance();
|
||||
|
||||
const {state} = useLocation() as {state: BSVersion};
|
||||
const navigate = useNavigate();
|
||||
const bsLauncherService = BSLauncherService.getInstance();
|
||||
const {state} = useLocation() as {state: BSVersion};
|
||||
const navigate = useNavigate();
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||
|
||||
const [oculusMode, setOculusMode] = useState(false);
|
||||
const [desktopMode, setDesktopMode] = useState(false);
|
||||
const [debugMode, setDebugMode] = useState(false);
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||
const launchState = useObservable(bsLauncherService.launchState$);
|
||||
const navigateToVersion = (version: BSVersion) => navigate(`/bs-version/${version.BSVersion}`, {state: version});
|
||||
const openFolder = () => ipcService.sendLazy("bs-version.open-folder", {args: state});
|
||||
const exportMaps = () => mapService.exportVersionMaps(state);
|
||||
const verifyFiles = () => bsDownloaderService.download(state, true);
|
||||
|
||||
useEffect(() => {
|
||||
setOculusMode(!!configService.get<boolean>(LaunchMods.OCULUS_MOD));
|
||||
setDesktopMode(!!configService.get<boolean>(LaunchMods.DESKTOP_MOD));
|
||||
setDebugMode(!!configService.get<boolean>(LaunchMods.DEBUG_MOD));
|
||||
}, []);
|
||||
|
||||
const navigateToVersion = (version: BSVersion) => {
|
||||
navigate(`/bs-version/${version.BSVersion}`, {state: version});
|
||||
}
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
if(mode === LaunchMods.DEBUG_MOD){ setDebugMode(value); }
|
||||
else if(mode === LaunchMods.OCULUS_MOD){
|
||||
setOculusMode(value);
|
||||
setDesktopMode(false);
|
||||
configService.set(LaunchMods.DESKTOP_MOD, false);
|
||||
const uninstall = async () => {
|
||||
const modalCompleted = await modalService.openModal(ModalType.UNINSTALL, state)
|
||||
if(modalCompleted.exitCode === ModalExitCode.COMPLETED){
|
||||
bsUninstallerService.uninstall(state).then(() => {
|
||||
bsVersionManagerService.askInstalledVersions();
|
||||
const newVersionPage = bsVersionManagerService.getInstalledVersions()[0];
|
||||
navigateToVersion(newVersionPage);
|
||||
})
|
||||
}
|
||||
}
|
||||
else if(mode === LaunchMods.DESKTOP_MOD){
|
||||
setDesktopMode(value);
|
||||
setOculusMode(false);
|
||||
configService.set(LaunchMods.OCULUS_MOD, false);
|
||||
|
||||
const edit = () => {
|
||||
bsVersionManagerService.editVersion(state).then(newVersion => {
|
||||
if(!newVersion){ return; }
|
||||
navigateToVersion(newVersion);
|
||||
});
|
||||
}
|
||||
configService.set(mode, value);
|
||||
}
|
||||
|
||||
const openFolder = () => {
|
||||
ipcService.sendLazy("bs-version.open-folder", {args: state});
|
||||
}
|
||||
|
||||
const uninstall = async () => {
|
||||
const modalCompleted = await modalService.openModal(ModalType.UNINSTALL, state)
|
||||
if(modalCompleted.exitCode === ModalExitCode.COMPLETED){
|
||||
bsUninstallerService.uninstall(state)
|
||||
.then(() => {
|
||||
bsVersionManagerService.askInstalledVersions();
|
||||
const newVersionPage = bsVersionManagerService.getInstalledVersions()[0];
|
||||
navigateToVersion(newVersionPage);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const verifyFiles = () => {
|
||||
bsDownloaderService.download(state, true);
|
||||
}
|
||||
|
||||
const launchBs = () => {
|
||||
bsLauncherService.launch(state, oculusMode, desktopMode, debugMode);
|
||||
}
|
||||
|
||||
const edit = () => {
|
||||
bsVersionManagerService.editVersion(state).then(newVersion => {
|
||||
if(!newVersion){ return; }
|
||||
navigateToVersion(newVersion);
|
||||
});
|
||||
}
|
||||
|
||||
const clone = () => {
|
||||
bsVersionManagerService.cloneVersion(state).then(newVersion => {
|
||||
@@ -103,9 +59,6 @@ export function VersionViewer() {
|
||||
});
|
||||
}
|
||||
|
||||
const exportMaps = () => {
|
||||
mapService.exportVersionMaps(state);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -113,33 +66,19 @@ export function VersionViewer() {
|
||||
<div className="relative flex items-center flex-col w-full h-full text-gray-200 backdrop-blur-lg">
|
||||
<BsmImage className='relative object-cover h-28' image={BSLogo}/>
|
||||
<h1 className='relative text-4xl font-bold italic -top-3'>{state.name ? `${state.BSVersion} - ${state.name}` : state.BSVersion}</h1>
|
||||
<TabNavBar className='mt-3' tabsText={["misc.launch", "misc.maps", "misc.mods"]} onTabChange={(i : number) => setCurrentTabIndex(i)}/>
|
||||
<div className='mt-2 w-full grow flex transition-transform duration-300 pt-5' style={{transform: `translate(${-(currentTabIndex * 100)}%, 0)`}}>
|
||||
<div className='w-full shrink-0 items-center relative flex flex-col justify-start -top-2'>
|
||||
<div className='grid grid-flow-col grid-cols-3 gap-6'>
|
||||
<ToogleLunchMod icon='oculus' onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus"/>
|
||||
<ToogleLunchMod icon='desktop' onClick={() => setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/>
|
||||
<ToogleLunchMod icon='terminal' onClick={() => setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug"/>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton onClick={launchBs} active={JSON.stringify(state) === JSON.stringify(launchState)} className='relative text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 w-full h-full flex justify-center'>
|
||||
<div className='p-4 bg-light-main-color-2 dark:bg-main-color-2 h-fit rounded-md'>
|
||||
<img src={wipGif} alt="" />
|
||||
<span className='block w-full text-center font-bold mt-2 text-gray-800 dark:text-white'>Work In Progress</span>
|
||||
</div>
|
||||
</div>
|
||||
<TabNavBar className='my-3' tabsText={["misc.launch", "misc.maps", "misc.mods"]} onTabChange={(i : number) => setCurrentTabIndex(i)}/>
|
||||
<div className='mt-2 w-full min-h-0 grow flex transition-transform duration-300 pt-3' style={{transform: `translate(${-(currentTabIndex * 100)}%, 0)`}}>
|
||||
<LaunchSlide version={state}/>
|
||||
<div className='shrink-0 w-full h-full flex justify-center'>
|
||||
<div className='p-4 bg-light-main-color-2 dark:bg-main-color-2 h-fit rounded-md'>
|
||||
<img src={wipGif} alt="" />
|
||||
<span className='block w-full text-center font-bold mt-2 text-gray-800 dark:text-white'>Work In Progress</span>
|
||||
</div>
|
||||
</div>
|
||||
<ModsSlide version={state}/>
|
||||
</div>
|
||||
</div>
|
||||
<BsmDropdownButton className='absolute top-5 right-5 h-9 w-9' items={[
|
||||
<BsmDropdownButton className='absolute top-5 right-5 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md' items={[
|
||||
{text: "pages.version-viewer.dropdown.open-folder", icon: "folder", onClick: openFolder},
|
||||
(!state.steam && {text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles}),
|
||||
(!state.steam && {text: "Exporter les misc.maps", icon: "export", onClick: exportMaps}),
|
||||
@@ -149,19 +88,4 @@ export function VersionViewer() {
|
||||
]}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ToogleLunchMod(props: {onClick: () => void, active: boolean, text: string, icon: BsmIconType}) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!props.active && "shadow-md shadow-black"}`} onClick={() => props.onClick()}>
|
||||
<div className={`absolute glow-on-hover rounded-full ${props.active && "opacity-100 blur-[2px]"}`}/>
|
||||
<div className='w-full h-full pl-6 pr-6 flex justify-center items-center bg-light-main-color-2 dark:bg-main-color-2 p-3 rounded-full text-gray-800 dark:text-white group-hover:bg-light-main-color-1 dark:group-hover:bg-main-color-1'>
|
||||
<BsmIcon icon={props.icon} className='mr-1 h-7 text-gray-800 dark:text-white'/>
|
||||
<span className='w-fit min-w-fit h-full text-lg font-bold uppercase tracking-wide italic'>{t(props.text)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export class BsDownloaderService{
|
||||
this.progressBarService.hide(true);
|
||||
this.resetDownload();
|
||||
if(res.success && isFirstCall){ this.notificationService.notifySuccess({title: `notifications.bs-download.success.titles.${isVerification ? "verification-finished" : "download-success"}`, duration: 3000}); }
|
||||
else if(res.data && isFirstCall){ console.log("aaaa"); this.notificationService.notifyError({title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000}); }
|
||||
else if(res.data && isFirstCall){ this.notificationService.notifyError({title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000}); }
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export class BSLauncherService{
|
||||
|
||||
public launch(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean): Promise<NotificationResult|string>{
|
||||
const lauchOption: LauchOption = {debug, oculus, desktop, version};
|
||||
if(this.launchState$.value){ return this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.BS_ALREADY_RUNNING"}); }
|
||||
this.launchState$.next(version);
|
||||
return this.ipcService.send<LaunchResult>("bs-launch.launch", {args: lauchOption}).then(res => {
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { InstallModsResult, UninstallModsResult } from "shared/models/mods";
|
||||
import { Mod, ModInstallProgression } from "shared/models/mods";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ModalExitCode, ModalService, ModalType } from "./modale.service";
|
||||
import { NotificationService, NotificationType } from "./notification.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
|
||||
export class BsModsManagerService {
|
||||
|
||||
private static instance: BsModsManagerService;
|
||||
|
||||
private readonly NOTIFICATION_DURATION = 3000;
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly modals: ModalService;
|
||||
private readonly notifications: NotificationService;
|
||||
|
||||
public readonly isInstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
public readonly isUninstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
public static getInstance(): BsModsManagerService{
|
||||
if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); }
|
||||
return BsModsManagerService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.modals = ModalService.getInsance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Promise<Mod[]>{
|
||||
return this.ipcService.send<Mod[], BSVersion>("get-available-mods", {args: version}).then(res => res.data);
|
||||
}
|
||||
|
||||
public getInstalledMods(version: BSVersion): Promise<Mod[]>{
|
||||
return this.ipcService.send<Mod[], BSVersion>("get-installed-mods", {args: version}).then(res => res.data);
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Promise<void>{
|
||||
if(!this.progressBar.require()){ return new Promise(res => res()); }
|
||||
|
||||
const progress$: Observable<ProgressionInterface> = this.ipcService.watch<ModInstallProgression>("mod-installed").pipe(map(res => {
|
||||
return {progression: res.data.progression, label: res.data.name} as ProgressionInterface
|
||||
}));
|
||||
|
||||
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
|
||||
|
||||
this.isInstalling$.next(true);
|
||||
return this.ipcService.send<InstallModsResult, {mods: Mod[], version: BSVersion}>("install-mods", {args: {mods, version}}).then(res => {
|
||||
|
||||
if(res.success && res.data){
|
||||
const isFullyInstalled = res.data.nbInstalledMods === res.data.nbModsToInstall;
|
||||
const title = `notifications.mods.install-mods.titles.${isFullyInstalled ? "success" : "warning"}`;
|
||||
const desc = `notifications.mods.install-mods.msg.${isFullyInstalled ? "success" : "warning"}`;
|
||||
|
||||
this.notifications.notify({type: isFullyInstalled ? NotificationType.SUCCESS : NotificationType.WARNING, title, desc, duration: this.NOTIFICATION_DURATION});
|
||||
}
|
||||
else{
|
||||
this.notifications.notifyError({title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${res.error}`, duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
this.isInstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
public async uninstallMod(mod: Mod, version: BSVersion): Promise<void>{
|
||||
|
||||
if(!this.progressBar.require()){ return; }
|
||||
|
||||
const modalRes = await this.modals.openModal(ModalType.UNINSTALL_MOD, mod);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
|
||||
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
|
||||
|
||||
this.isUninstalling$.next(true);
|
||||
return this.ipcService.send("uninstall-mods", {args: {mods: [mod], version}}).then(res => {
|
||||
|
||||
if(res.success){
|
||||
this.notifications.notifySuccess({title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
else{
|
||||
this.notifications.notifyError({title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${res.error}`, duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
|
||||
public async uninstallAllMods(version: BSVersion){
|
||||
|
||||
if(!this.progressBar.require()){ return; }
|
||||
|
||||
const modalRes = await this.modals.openModal(ModalType.UNINSTALL_ALL_MODS, version);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
|
||||
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
|
||||
|
||||
this.isUninstalling$.next(true);
|
||||
return this.ipcService.send<UninstallModsResult>("uninstall-all-mods", {args: version}).then(res => {
|
||||
|
||||
if(res.success){
|
||||
this.notifications.notifySuccess({title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
else{
|
||||
this.notifications.notifyError({title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${res.error}`, duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,10 @@ export class I18nService {
|
||||
public translate(translationKey: string, args?: Record<string, string>): string{
|
||||
let translated = this.cache.get(translationKey);
|
||||
if(!translated){
|
||||
console.log(translationKey);
|
||||
translated = getProperty(this.dictionary, translationKey);
|
||||
translated ? this.cache.set(translated, translationKey) : translated = translationKey;
|
||||
}
|
||||
args && Object.keys(args).forEach(key => translated = translated.replaceAll(`{${key}}`, args[key]));
|
||||
args && Object.keys(args).forEach(key => {translated = translated.replaceAll(`{${key}}`, args[key])});
|
||||
return translated;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IpcService } from "./ipc.service";
|
||||
|
||||
export class LinkOpenerService{
|
||||
|
||||
private static instance: LinkOpenerService;
|
||||
|
||||
private readonly ipcService: IpcService
|
||||
|
||||
public static getInstance(): LinkOpenerService{
|
||||
if(!LinkOpenerService.instance){ LinkOpenerService.instance = new LinkOpenerService(); }
|
||||
return LinkOpenerService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
}
|
||||
|
||||
public open(url: string): void{
|
||||
this.ipcService.sendLazy("new-window", {args: url});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,23 +50,25 @@ export class ModalService{
|
||||
|
||||
}
|
||||
|
||||
export enum ModalType {
|
||||
export const enum ModalType {
|
||||
STEAM_LOGIN = "STEAM_LOGIN",
|
||||
GUARD_CODE = "GUARD_CODE",
|
||||
UNINSTALL = "UNINSTALL",
|
||||
INSTALLATION_FOLDER = "INSTALLATION_FOLDER",
|
||||
EDIT_VERSION = "EDIT_VERSION",
|
||||
CLONE_VERSION = "CLONE_VERSION"
|
||||
CLONE_VERSION = "CLONE_VERSION",
|
||||
UNINSTALL_MOD = "UNINSTALL_MOD",
|
||||
UNINSTALL_ALL_MODS = "UNINSTALL_ALL_MODS",
|
||||
}
|
||||
|
||||
export enum ModalExitCode {
|
||||
export const enum ModalExitCode {
|
||||
NO_CHOICE = -1,
|
||||
COMPLETED = 0,
|
||||
CLOSED = 1,
|
||||
CANCELED = 2,
|
||||
}
|
||||
|
||||
export interface ModalResponse<T> {
|
||||
export interface ModalResponse<T = unknown> {
|
||||
exitCode: ModalExitCode,
|
||||
data?: T
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
export class PageStateService {
|
||||
|
||||
private static instance: PageStateService;
|
||||
|
||||
private readonly _state$: BehaviorSubject<any> = new BehaviorSubject(undefined);
|
||||
|
||||
public static getInstance(): PageStateService{
|
||||
if(!PageStateService.instance){ PageStateService.instance = new PageStateService(); }
|
||||
return PageStateService.instance;
|
||||
}
|
||||
|
||||
private constructor(){}
|
||||
|
||||
public setState(state: unknown){
|
||||
this._state$.next(state);
|
||||
}
|
||||
|
||||
public getState<T = unknown>(): T{
|
||||
return this._state$.value;
|
||||
}
|
||||
|
||||
public get state$(){
|
||||
return this._state$.asObservable();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { distinctUntilChanged, map } from "rxjs/operators";
|
||||
import { BehaviorSubject, Observable, Subscription, timer } from "rxjs";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { CSSProperties } from "react";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
|
||||
export class ProgressBarService{
|
||||
|
||||
@@ -10,8 +12,9 @@ export class ProgressBarService{
|
||||
private readonly ipcService: IpcService;
|
||||
private notificationService: NotificationService;
|
||||
|
||||
private readonly _progression$: BehaviorSubject<number>;
|
||||
private readonly _progression$: BehaviorSubject<ProgressionInterface>;
|
||||
private readonly _visible$: BehaviorSubject<boolean>;
|
||||
private readonly _style$: BehaviorSubject<CSSProperties>;
|
||||
|
||||
private subscription: Subscription;
|
||||
|
||||
@@ -24,51 +27,55 @@ export class ProgressBarService{
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.notificationService = NotificationService.getInstance();
|
||||
|
||||
this._progression$ = new BehaviorSubject<number>(0);
|
||||
this._progression$ = new BehaviorSubject<ProgressionInterface>({progression: 0});
|
||||
this._visible$ = new BehaviorSubject<boolean>(false);
|
||||
this._style$ = new BehaviorSubject<CSSProperties>(undefined);
|
||||
|
||||
this.progression$.subscribe(progression => this.setSystemProgression(progression));
|
||||
this.progress$.subscribe(value => this.setSystemProgression(value));
|
||||
}
|
||||
|
||||
private setSystemProgression(progression: number){
|
||||
this.ipcService.sendLazy("window.progression", {args: progression});
|
||||
}
|
||||
|
||||
public subscribreTo(obs: Observable<number>){
|
||||
public subscribreTo(obs: Observable<ProgressionInterface|number>){
|
||||
if(this.subscription){ this.unsubscribe(); }
|
||||
this.subscription = obs.pipe(distinctUntilChanged()).subscribe(value => {
|
||||
this.progression$.next(value);
|
||||
if(typeof value === "number"){ return this._progression$.next({progression: value}); }
|
||||
this._progression$.next(value);
|
||||
});
|
||||
}
|
||||
|
||||
public unsubscribe(){
|
||||
this._progression$.next(0);
|
||||
this._progression$.next({progression: 0});
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
public show(obs?: Observable<number>, unsubscribe? : boolean){
|
||||
public show(obs?: Observable<ProgressionInterface|number>, unsubscribe? : boolean, style?: CSSProperties){
|
||||
if(unsubscribe){ this.unsubscribe(); }
|
||||
if(obs){ this.subscribreTo(obs); }
|
||||
this.visible$.next(true);
|
||||
this._visible$.next(true);
|
||||
this._style$.next(style);
|
||||
}
|
||||
|
||||
public showFake(speed: number): void{
|
||||
const obs = timer(1000, 100).pipe(map(val => {
|
||||
if(this.progression$.value >= 100){ return 100; }
|
||||
public showFake(speed: number, style?: CSSProperties): void{
|
||||
const obs: Observable<ProgressionInterface> = timer(1000, 100).pipe(map(val => {
|
||||
if(this._progression$.value.progression >= 100){ return {progression: 100}; }
|
||||
const currentProgress = speed * (val + 1);
|
||||
return Math.round(Math.atan(currentProgress) / (Math.PI / 2) * 100 * 1000) / 1000;
|
||||
const progress = Math.round(Math.atan(currentProgress) / (Math.PI / 2) * 100 * 1000) / 1000;
|
||||
return {progression: progress} as ProgressionInterface;
|
||||
}));
|
||||
this.show(obs, true);
|
||||
this.show(obs, true, style);
|
||||
}
|
||||
|
||||
public complete(): void{
|
||||
this.progression$.next(100);
|
||||
this._progression$.next({progression: 100});
|
||||
}
|
||||
|
||||
public hide(unsubscribe: boolean){
|
||||
public hide(unsubscribe: boolean = true){
|
||||
if(unsubscribe){ this.unsubscribe(); }
|
||||
this.visible$.next(false);
|
||||
this._visible$.next(false);
|
||||
}
|
||||
|
||||
public require(): boolean{
|
||||
@@ -79,9 +86,13 @@ export class ProgressBarService{
|
||||
return true;
|
||||
}
|
||||
|
||||
public get progression$(): BehaviorSubject<number>{ return this._progression$; }
|
||||
public get visible$(): BehaviorSubject<boolean>{ return this._visible$; }
|
||||
public get progressData$(): Observable<ProgressionInterface>{ return this._progression$.asObservable(); }
|
||||
public get progress$(): Observable<number>{ return this._progression$.pipe(map(data => data.progression)); }
|
||||
public get progressLabel(): Observable<string>{ return this._progression$.pipe(map(data => data?.label)); }
|
||||
|
||||
public get visible$(): Observable<boolean>{ return this._visible$.asObservable(); }
|
||||
public get isVisible(): boolean{ return this._visible$.value; }
|
||||
public get style$(): Observable<CSSProperties>{ return this._style$.asObservable(); }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ModInstallProgression, InstallModsResult, UninstallModsResult } from "./mod-ipc.model"
|
||||
export { Mod, DownloadLink, DownloadLinkType, FileHashes, ModAuthor } from "./mod.interface"
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface ModInstallProgression{
|
||||
name: string,
|
||||
progression: number
|
||||
}
|
||||
|
||||
export interface InstallModsResult {
|
||||
nbModsToInstall: number,
|
||||
nbInstalledMods: number
|
||||
}
|
||||
|
||||
export interface UninstallModsResult {
|
||||
nbModsToUninstall: number,
|
||||
nbUninstalledMods: number
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export interface Mod {
|
||||
_id: string,
|
||||
name: string,
|
||||
version: string,
|
||||
gameVersion: string,
|
||||
authorId: string,
|
||||
uploadedDate: string,
|
||||
updatedDate: string,
|
||||
author: ModAuthor,
|
||||
description: string,
|
||||
link: string,
|
||||
category: string,
|
||||
downloads: DownloadLink[],
|
||||
required: boolean,
|
||||
dependencies: Mod[],
|
||||
status: string
|
||||
|
||||
}
|
||||
|
||||
export interface ModAuthor {
|
||||
_id: string,
|
||||
username: string,
|
||||
lastLogin: string,
|
||||
}
|
||||
|
||||
export interface DownloadLink {
|
||||
type: DownloadLinkType,
|
||||
url: string,
|
||||
hashMd5: FileHashes[]
|
||||
}
|
||||
|
||||
export type DownloadLinkType = "universal"|"steam"|"oculus";
|
||||
|
||||
export interface FileHashes {
|
||||
hash: string,
|
||||
file: string
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProgressionInterface } from "./progress-bar.model"
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ProgressionInterface {
|
||||
progression: number,
|
||||
label?: string
|
||||
};
|
||||
+1
-4
@@ -6,10 +6,7 @@ module.exports = {
|
||||
darkMode: 'class',
|
||||
content: ['./src/renderer/**/*.tsx'],
|
||||
mode: 'jit',
|
||||
plugins: [require('tailwind-scrollbar-hide'), require('tailwind-scrollbar')],
|
||||
variants: {
|
||||
scrollbar: ['rounded']
|
||||
},
|
||||
plugins: [require('tailwind-scrollbar-hide'), require('tailwind-scrollbar')({ nocompatible: true }), require("tailwindcss-scoped-groups")({groups: ["one"]})],
|
||||
theme:{
|
||||
colors:{
|
||||
...colors,
|
||||
|
||||
Reference in New Issue
Block a user