Compare commits

..
80 Commits
Author SHA1 Message Date
HorizonCode b111cf4fa4 fix building scripts, update electron 2023-05-04 21:47:21 +02:00
HorizonCode e2713179c8 bump version 2023-05-04 21:29:22 +02:00
HorizonCode 92ee92d636 reformat index 2023-05-04 21:28:04 +02:00
HorizonCode b3463378e9 reformat index 2023-05-04 21:23:22 +02:00
HorizonCode 00e3e80d43 fix ignored list 2023-05-04 21:23:06 +02:00
HorizonCode 2df9f533fe set buttons to constant array 2023-05-04 21:22:51 +02:00
HorizonCode cc93e24ab6 update action 2023-05-04 21:22:32 +02:00
HorizonCode 48cd81f28d update url 2023-05-04 21:22:22 +02:00
HorizonCode 068d02f1f7 remove logging, exclude certain osu files from updating 2023-05-04 21:07:09 +02:00
HorizonCode e725592ef1 remove patch checkbox for now 2023-05-04 21:05:01 +02:00
HorizonCode 6bc0b215d7 change action button links 2023-05-04 21:04:48 +02:00
HorizonCode da1b05c9a1 bg tokyo night style 2023-01-25 15:50:50 +01:00
HorizonCode f1ef41dfea Update 'README.md' 2022-12-31 06:37:01 +00:00
HorizonCode a358ff08fa Merge branch 'master' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher 2022-12-23 21:43:34 +01:00
HorizonCode 5db981228e distro info for linux 2022-12-23 21:41:47 +01:00
HorizonCode 3247a3fcdf reformat and add distro check for linux 2022-12-23 21:41:39 +01:00
HorizonCode 7b9a6d77ab change colors to tokyo night 2022-12-23 21:41:23 +01:00
HorizonCode 337a8af68f add util method to detect osu folder, still needs to be tested on different systems. 2022-10-27 16:28:22 +02:00
HorizonCode 84c13331d2 add launch button folder choose if not set 2022-10-27 15:42:45 +02:00
HorizonCode e77cb22028 increase height of window 2022-10-27 15:42:29 +02:00
HorizonCode b673b74eee make checkbox disabled for now 2022-10-27 14:41:26 +02:00
HorizonCode 53a2fb55a6 forgot to revert display 2022-10-27 14:35:51 +02:00
HorizonCode 3e4186d3a7 added checkbox for patching 2022-10-27 14:30:46 +02:00
HorizonCode 89cdc8f316 preperations for the patch method 2022-10-27 13:58:15 +02:00
HorizonCode 67a53fdd2d readd rebuild script for get-window-by-name 2022-10-27 11:00:10 +02:00
HorizonCode 78c554be2d add editing status 2022-10-27 06:38:58 +02:00
HorizonCode 586857146c remove unused scripts 2022-10-27 06:38:14 +02:00
HorizonCode 2cea272836 make server logo non-draggable 2022-10-27 06:38:03 +02:00
HorizonCode 02d4fa2199 add package issue notification 2022-10-27 06:37:48 +02:00
HorizonCode 72a0d9b780 Update 'README.md' 2022-10-24 12:38:32 +00:00
HorizonCode 1489d44a31 Upload files to 'preview' 2022-10-24 12:36:16 +00:00
HorizonCode adaaf32acf remove rebuild script usage, add other used libs i forgot to add 2022-10-24 12:29:33 +00:00
HorizonCode 033ce8f3a4 make playing status work under linux 2022-10-24 06:36:03 +02:00
HorizonCode 2857345997 only update currentState on certain types 2022-10-24 05:46:22 +02:00
HorizonCode 4f3eaddfc7 style sweetalert2 2022-10-23 17:31:32 +02:00
HorizonCode 573bcd393c add notice under Linux 2022-10-23 17:07:20 +02:00
HorizonCode 11c3f9f657 forgot to remove image tag in html 2022-10-23 17:01:39 +02:00
HorizonCode c3c5390830 fix user images, many thanks to Wopiie! 2022-10-23 01:31:17 +02:00
HorizonCode 67de9b7c16 remove wincript(PASSWORD IS PLAIN IN CONFIG FOR NOW!) make linux compatible 2022-10-23 01:08:58 +02:00
HorizonCode 37567b5767 bump version 2022-10-21 00:59:19 +02:00
HorizonCode 0a780b84e4 forgor to remove debug log 2022-10-21 00:59:11 +02:00
HorizonCode c8a07fbb0d add basic discord rpc, showing osu status 2022-10-21 00:57:42 +02:00
HorizonCode 1de9fce504 show error modal when update failed 2022-10-20 19:08:48 +02:00
HorizonCode 73fb9464d2 bump version 2022-10-18 01:46:11 +02:00
HorizonCode 5c819938c8 login mechanics and launch is working now 2022-10-18 01:46:04 +02:00
HorizonCode 4f84dd3248 add encrypt test 2022-10-18 01:45:17 +02:00
HorizonCode 2fa6a9b3d5 push electron to latest 2022-10-17 21:31:48 +02:00
HorizonCode 258c01ccc7 add build from source section to readme 2022-10-17 21:30:46 +02:00
HorizonCode d80d6dc889 make logo on lauch page a bit bigger 2022-10-17 20:19:22 +02:00
HorizonCode 7a8495d88a downgrade electron to propper version, add rebuild script 2022-10-17 20:10:04 +02:00
HorizonCode 9ac75a309b update electron, add dpapi function wip 2022-10-17 18:10:17 +02:00
HorizonCode 8ef809be5c add loading page on startup 2022-10-17 16:37:12 +02:00
HorizonCode e6ccec3e93 do previous page fadeout and not immediately hide 2022-10-17 12:58:56 +02:00
HorizonCode 7785ebec92 fixed user image misplacement 2022-10-17 12:58:07 +02:00
HorizonCode 3edf2ce8a3 new design rollout, function still not given 2022-10-17 12:47:47 +02:00
HorizonCode 2b12654825 set author 2022-10-16 23:19:24 +02:00
HorizonCode 293ea1b798 disable set account button 2022-10-16 23:18:21 +02:00
HorizonCode 5f36de6697 add appInfo 2022-10-16 23:16:05 +02:00
HorizonCode a0628cc873 remove/disable console logs 2022-10-16 23:10:55 +02:00
HorizonCode 5c87704b10 launch mechanics working 2022-10-16 23:10:07 +02:00
HorizonCode 72958950b7 adjust window height 2022-10-16 22:51:16 +02:00
HorizonCode 8d29bd6822 whole update logic working 2022-10-16 22:44:56 +02:00
HorizonCode c2a75319dc add alerts dark theme 2022-10-16 22:21:02 +02:00
HorizonCode 2de6cc11cf fix folder detection on launch 2022-10-16 22:20:50 +02:00
HorizonCode 1b8dfbad03 add alerts, darkmode wip 2022-10-16 21:31:05 +02:00
HorizonCode 5565892155 add updating check 2022-10-16 21:13:30 +02:00
HorizonCode 2bf98db729 get updated files from osu 2022-10-16 18:52:34 +02:00
HorizonCode 830b1099ac folder check 2022-10-16 18:42:04 +02:00
HorizonCode 14201fb9cd fix layout 2022-10-16 18:41:53 +02:00
HorizonCode bd7f3ddefd config util 2022-10-16 18:41:24 +02:00
HorizonCode 99d0d927e0 get osu config method 2022-10-16 18:41:15 +02:00
HorizonCode 356ec5d7e0 cancel if result is undefined 2022-10-16 16:47:43 +02:00
HorizonCode 071b13ee6e change pipe name 2022-10-16 16:47:31 +02:00
HorizonCode 283e125e38 detect valid osu folder by matches 2022-10-16 16:47:18 +02:00
HorizonCode 65b607fb31 update readme 2022-10-16 16:15:35 +02:00
HorizonCode 1c6c9a2d6e launch debug window detatched 2022-10-16 16:06:34 +02:00
HorizonCode b84ddaf1b7 set launch button disabled for now 2022-10-16 16:06:20 +02:00
HorizonCode 3baa262ca6 fix window bounds, use modern method to cancel new windows 2022-10-16 16:06:06 +02:00
HorizonCode c648cfd2b3 reinit 2022-10-16 15:55:47 +02:00
HorizonCode d50cdd2a77 initial commit 2022-10-16 15:54:29 +02:00
188 changed files with 3440 additions and 33618 deletions

No files matched your search

+4 -10
View File
@@ -1,10 +1,4 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
node_modules/
release/
yarn.lock
pnpm-lock.yaml
-12
View File
@@ -1,12 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
# Tauri
/src-tauri/
-29
View File
@@ -1,29 +0,0 @@
{
"bracketSameLine": false,
"insertPragma": false,
"arrowParens": "always",
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"plugins": ["prettier-plugin-svelte", "@trivago/prettier-plugin-sort-imports"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
},
{
"files": "*.{css,pcss}",
"options": {
"tabWidth": 2,
"useTabs": false,
"singleQuote": false
}
}
]
}
-13
View File
@@ -1,13 +0,0 @@
{
"recommendations": [
"svelte.svelte-vscode",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"esbenp.prettier-vscode",
"fill-labs.dependi",
"mylesmurphy.prettify-ts",
"edwinhuish.better-comments-next",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss"
]
}
-18
View File
@@ -1,18 +0,0 @@
{
"svelte.enable-ts-plugin": true,
"git.enableCommitSigning": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"json.schemaDownload.trustedDomains": {
"https://schemastore.azurewebsites.net/": true,
"https://raw.githubusercontent.com/": true,
"https://www.schemastore.org/": true,
"https://json.schemastore.org/": true,
"https://json-schema.org/": true,
"https://schema.tauri.app/": true
}
}
+235
View File
@@ -0,0 +1,235 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based on the Program.
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
<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 Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.
-673
View File
@@ -1,673 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C) 2025 HorizonCode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
EZPPLauncher Copyright (C) 2025 HorizonCode
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+25 -139
View File
@@ -1,150 +1,36 @@
# EZPPLauncher
Welcome to the EZPPLauncher! A new way to connect to the EZPPFarm server.
**EZPPLauncher** is a custom launcher for the private osu! server [EZPPFarm](https://ez-pp.farm), built with **Rust** and **Svelte** using the [Tauri](https://tauri.app) framework.
It enhances the osu! experience with quality-of-life features and integration specifically designed for the EZPPFarm community.
Just one click and you are ready to go!
---
## Preview Image
![preview](https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/raw/branch/master/preview/EZPPLauncher.png)
## 🖼️ Preview
## Installation
The Launcher is a "plug and play thing", download it, place it on the desktop and execute!
![EZPPLauncher Preview](./assets/EZPPLauncher-preview.png)
## Features
---
* Automatic osu! client updating before Launch
* Account saving
## 🚀 Features
## Used Libraries
- [x] Automatically updates osu! before launching
- [x] Automatically start/stop the OpenTabletDriver Daemon (Windows)
- [x] Automatic ingame login
- [x] Patches the osu! client to show misses in Relax and Autopilot (Windows, Linux and macOS via Wine)
- [x] Displays your EZPPFarm stats in the launcher
- [x] Shows the number of imported beatmapsets
- [x] Shows the number of imported skins
- [x] Displays ping to the EZPPFarm server
- [x] Discord Rich Presence
- [x] Performance Display Overlay (currently in experimental patcher release stream)
- [x] Launcher Theming (API 1.0 bundles and API 2.0 declarative themes)
- [x] Advanced theme editor in its own window with freeform text/image/video/shape layers, multi-select and snapping, layer locks, draft recovery, validation and export
- [x] Theme update backups with one-click rollback, plus a trust prompt before running executable API 1.0 themes
- [x] In-app diagnostics for osu!, Wine, the patcher, and the experimental stream's .NET 8 runtime, plus a log-bundle viewer
* [Electron](https://www.npmjs.com/package/electron)
* [custom-electron-titlebar](https://www.npmjs.com/package/custom-electron-titlebar)
* [axios](https://www.npmjs.com/package/axios)
* [jquery](https://www.npmjs.com/package/jquery)
* [discord-auto-rpc](https://www.npmjs.com/package/discord-auto-rpc)
* [get-window-by-name](https://www.npmjs.com/package/get-window-by-name)
* [sweetalert2](https://www.npmjs.com/package/sweetalert2)
* [node-downloader-helper](https://www.npmjs.com/package/node-downloader-helper)
---
## Build from source
## 💻 Supported Platforms
- clone repo
- cd into the repo
- use `yarn install` to install all dependencies
- use the buildscript `pack-win` to build a executeable
| Platform | Status |
| -------- | ------------------------------------------------------ |
| Windows | ✅ Supported |
| macOS | 🕧 Partially supported (via Wineskin/Whisky/CrossOver) |
| Linux | 🕧 Partially supported (via osu-winello) |
> **Windows** remains the fully supported platform. OpenTabletDriver is Windows-only. On macOS,
> in-game patching runs the Windows patcher through the same Wine prefix as osu!. The stable
> patcher stream needs no extra runtime; the experimental stream needs the .NET 8 Desktop Runtime
> installed in that prefix.
### macOS
osu! stable is a Windows build, so on macOS it runs inside a Wine wrapper. EZPPLauncher
supports Wineskin wrappers (the usual `osu!.app` wine skin), Whisky bottles and CrossOver
bottles.
The setup wizard automatically looks for Wineskins in `/Applications` and `~/Applications`.
If detection fails, choose **Browse** and pick the `osu!.app` wrapper itself (or a Wine prefix
folder); the launcher resolves the osu! installation inside it, so there is no need to open the
bundle's package contents.
Large VP8/VP9/AV1 theme videos are transcoded once to a cached 720p H.264/AAC file so WebKit
can use the Mac's hardware decoder. The first large video triggers a one-time (~63 MB) download
of a signed static FFmpeg 9.0.2 build from `ffmpeg.martin-riedl.de` into `~/.ezpplauncher/bin`;
the launcher verifies its pinned SHA-256 and makes it executable. The managed build requires
macOS 12 or newer. If the download fails, a system FFmpeg (Homebrew or MacPorts) is used; without
either, themes still load but large WebM videos remain on WebKit's software decoder and may
stutter. Theme authors can also ship an H.264/MP4 version alongside the WebM.
---
## 🪵 Logs
Launcher, osu! and patcher output is written to `.ezpplauncher/logs` in your home folder:
| File | Contents |
| -------------- | -------------------------------------------------------------- |
| `launcher.log` | Launcher lifecycle, webview console output and uncaught errors |
| `osu.log` | osu! and osu! updater output, including stdout/stderr |
| `patcher.log` | Patcher output, including stdout/stderr |
A file is only created when that stream actually writes a line. On each new session, or when
any file reaches 5 MiB, all three active files are bundled into a single
`logs_<date>_<time>.zip`; the five most recent bundles are kept. You can turn file logging off,
open the folder, or browse and filter a bundle in the built-in viewer from **Settings → Logging**.
On Windows the folder is `%USERPROFILE%\.ezpplauncher\logs`.
**Settings → Diagnostics** checks the selected osu! install and write access, the macOS Wine
prefix/runtime/version and (for the experimental patcher stream) its .NET 8 runtime, the FFmpeg
theme-video transcoder, osu-winello/wmctrl on Linux, and whether the patcher is present. It can
copy a plain-text support report or reopen the setup wizard for a guided repair.
---
## 🛠 Build From Source
### Requirements
- **Rust** (installed via [rustup](https://rustup.rs/) – recommended for Windows)
- **Cargo** (comes with rustup)
- **[CMake](https://cmake.org/download/)**
- **[Bun](https://bun.sh/)** (JavaScript runtime)
- On macOS: **Xcode Command Line Tools** (`xcode-select --install`)
### Steps
```bash
# Install all dependencies
bun i
# Build the project
bun run build
```
After building, the launcher executable will be located at:
```
# Windows
src-tauri/target/release/ezpplauncher.exe
# macOS
src-tauri/target/release/bundle/macos/EZPPLauncher.app
src-tauri/target/release/bundle/dmg/EZPPLauncher_*.dmg
```
---
## 📁 Project Structure (Simplified)
```
├── src/ # Frontend (Svelte)
├── src-tauri/ # Backend (Rust)
│ ├── src/macos.rs # Wine/Wineskin support for macOS
│ ├── tauri.conf.json # Tauri configuration
│ └── tauri.macos.conf.json # macOS bundle targets (app + dmg)
├── package.json # Bun project config
└── README.md
```
---
## 🎨 Launcher Themes
By default, the launcher includes several downloadable themes. Themes can be created
in two ways:
- Use the built-in **advanced editor** on the Themes screen. It opens in its own
resizable window with a full launcher home canvas, freeform text/image/video/shape
layers, WebM support, `@fontsource` fonts and custom font imports. It creates API 2.0
themes and can export them as shareable `.ezpplauncher-theme` files.
- Use the [create-ezpplauncher-theme](https://git.ez-pp.farm/EZPPFarm/create-ezpplauncher-theme)
CLI. It supports both API 1.0 Svelte themes and API 2.0 declarative themes.
See [THEME_API_V2.md](THEME_API_V2.md) for the full API 2.0 schema, limits and packaging
layout. API 1.0 themes continue to load unchanged.
## License
[AGPL-3.0](https://choosealicense.com/licenses/agpl-3.0/)
-440
View File
@@ -1,440 +0,0 @@
# EZPPLauncher Theme API 2.0
API 2.0 is a declarative theme format. Instead of shipping a JavaScript bundle that
mounts its own UI, a theme describes its background, effects, audio, fonts and
freeform layers in `theme.json`. The launcher renders it, and the built-in advanced
editor can create and edit these themes without writing code.
API 1.0 themes continue to work unchanged. Pick the API that fits the theme:
| | API 1.0 | API 2.0 |
| ----------------------- | ------------------------------------------------- | ---------------------------------------------- |
| Rendering | Svelte bundle (`entry`) | Launcher-rendered |
| Files | `theme.json`, `bundle.js`, `style.css`, `assets/` | `theme.json`, `assets/` |
| Created with | `create-ezpplauncher-theme` | Launcher editor or `create-ezpplauncher-theme` |
| Scripts / arbitrary DOM | Yes | No (data only, optional CSS) |
| Cross-platform | Depends on the bundle | Yes |
---
## Package layout
A packaged `.ezpplauncher-theme` file is a zip with this structure:
```
theme.json
assets/
background.webm
layer.png
music.mp3
font.woff2
preview.png # optional thumbnail
```
An installed theme lives in:
```
# Windows
C:\Users\<user>\.ezpplauncher\themes\<folder>\
# macOS / Linux
~/.ezpplauncher/themes/<folder>/
```
---
## `theme.json`
```json
{
"name": "Midnight",
"version": "1.0.0",
"apiVersion": "2.0",
"author": "Your Name",
"description": "A dark gradient with drifting stars.",
"preview": "preview.png",
"background": {
"type": "gradient",
"from": "#181825",
"to": "#3b1d4f",
"angle": 135,
"opacity": 1
},
"overlay": {
"color": "#000000",
"opacity": 0.2,
"vignette": 0.35
},
"effects": {
"type": "stars",
"density": 40,
"speed": 1,
"color": "#ffd6f2"
},
"typography": {
"body": "Sora Variable",
"heading": "Bebas Neue"
},
"fonts": [
{
"family": "My Display Font",
"src": "display.woff2",
"weight": "400",
"style": "normal"
}
],
"elements": [
{
"id": "tagline",
"type": "text",
"text": "MIDNIGHT",
"x": 640,
"y": 84,
"width": 300,
"height": 72,
"fontFamily": "Bebas Neue",
"fontSize": 46,
"fontWeight": 400,
"color": "#ffffff",
"align": "right",
"verticalAlign": "center",
"letterSpacing": 2
}
],
"audio": {
"src": "music.mp3",
"loop": true
},
"accentColor": "#ff006e",
"css": ""
}
```
### Metadata
| Field | Type | Required | Notes |
| ------------- | ------ | -------- | --------------------------------------------------- |
| `name` | string | yes | Display name; also used for the install folder |
| `version` | string | yes | Semver, used for update checks |
| `apiVersion` | string | yes | Must start with `2` (use `"2.0"`) |
| `author` | string | yes | Shown in the theme manager |
| `description` | string | no | Shown on the theme card |
| `nsfw` | bool | no | Marks the theme as adult content (`18+` badge) |
| `preview` | string | no | Card thumbnail; the editor generates `preview.png` |
| `accentColor` | string | no | Hex color used for focus rings and active UI states |
| `css` | string | no | Extra CSS injected while the theme is active |
### Background
`background.type` selects the layer:
```json
{ "type": "color", "color": "#181825", "opacity": 1 }
```
```json
{ "type": "gradient", "from": "#181825", "to": "#3b1d4f", "angle": 135, "opacity": 1 }
```
```json
{ "type": "image", "src": "background.png", "fit": "cover", "opacity": 1, "blur": 0 }
```
```json
{
"type": "video",
"src": "background.webm",
"fit": "cover",
"opacity": 1,
"blur": 0,
"muted": true
}
```
- `src` is relative to the theme's `assets/` folder.
- `fit` is `cover` (default) or `contain`.
- `opacity` is clamped to `0`–`1`, `blur` to `0`–`40` pixels.
- Background videos autoplay and loop. They are muted by default; set `muted` to `false`
to play their audio.
- Prefer **WebM** for video backgrounds and video layers. It is supported by the
launcher and keeps theme packages much smaller than MP4. MP4, MOV and M4V also
work.
### Overlay
```json
{ "color": "#000000", "opacity": 0.2, "vignette": 0.35 }
```
`opacity` controls the flat dim layer and `vignette` darkens the edges. Both are
clamped to `0`–`1`.
### Effects
```json
{ "type": "stars", "density": 40, "speed": 1, "color": "#ffd6f2" }
```
Supported types: `none`, `stars`, `snow`, `petals`, `bubbles`.
- `density` is clamped to `0`–`120` particles.
- `speed` is clamped to `0.2`–`3`.
- Effects are disabled when the user turns on **Reduce animations**.
### Audio
```json
{ "src": "music.mp3", "loop": true }
```
`src` is relative to `assets/`. Music uses the launcher's volume control and is
silent in the theme editor preview.
### Typography
`typography.body` changes the font used by the launcher UI. `typography.heading`
changes headings such as the launcher title.
```json
"typography": {
"body": "Montserrat Variable",
"heading": "Playfair Display Variable"
}
```
These families ship with the launcher through `@fontsource` and can be used
without adding any files:
| Family | Style |
| --------------------------- | ------- |
| `Sora Variable` | Sans |
| `Montserrat Variable` | Sans |
| `Bebas Neue` | Display |
| `Playfair Display Variable` | Serif |
| `Space Mono` | Mono |
| `JetBrains Mono Variable` | Mono |
Custom fonts can be packaged with the theme. The launcher generates the
`@font-face` rules from `fonts`:
```json
"fonts": [
{ "family": "My Display Font", "src": "display.woff2", "weight": "400", "style": "normal" }
]
```
- `src` is relative to `assets/`.
- Supported formats: WOFF2 (recommended), WOFF, TTF and OTF.
- `weight` defaults to `400`; `style` defaults to `normal`.
- Use the `family` value in `typography` and in text layers.
The editor's **Fonts** tab browses the full Fontsource catalog (2,000+ families)
and downloads the selected family, weight, style and subset as a WOFF2 into the
theme's `assets/` folder. It adds the matching `fonts` entry automatically, so
saved and exported themes stay self-contained.
### Freeform layers
`elements` are rendered above the launcher UI, but only on the **Home** screen.
The launcher canvas is a fixed **1000 × 650** coordinate space. Elements can extend
the home screen with text, images, videos and shapes, and they are
pointer-transparent in the running launcher so they never block buttons.
The order of the `elements` array controls stacking: the last element is drawn on
top. Themes can contain up to 100 layers.
Every layer supports:
| Field | Type | Notes |
| ---------- | ------- | ------------------------------------------------ |
| `id` | string | Unique within the theme |
| `name` | string? | Optional editor label; does not affect rendering |
| `type` | string | `text`, `image`, `video` or `shape` |
| `x`, `y` | number | Position in the 1000 × 650 stage |
| `width` | number | 24–1000 |
| `height` | number | 24–650 |
| `rotation` | number | Degrees, `-360`–`360` |
| `opacity` | number | `0`–`1` |
| `hidden` | boolean | Keep the layer in the file but do not render it |
| `locked` | boolean | Prevent moving/resizing the layer in the editor |
#### Text
```json
{
"id": "greeting",
"type": "text",
"text": "Welcome back!",
"x": 360,
"y": 220,
"width": 420,
"height": 90,
"fontFamily": "Bebas Neue",
"fontSize": 42,
"fontWeight": 600,
"color": "#ffffff",
"align": "center",
"verticalAlign": "center",
"lineHeight": 1.2,
"letterSpacing": 0
}
```
`align` is `left`, `center` or `right`; `verticalAlign` is `top`, `center` or
`bottom`. Font size is clamped to `6`–`240`.
#### Image and video
```json
{
"id": "logo",
"type": "image",
"src": "logo.png",
"x": 700,
"y": 420,
"width": 220,
"height": 140,
"fit": "contain",
"radius": 18,
"opacity": 1
}
```
```json
{
"id": "ambient",
"type": "video",
"src": "ambient.webm",
"x": 0,
"y": 0,
"width": 1000,
"height": 650,
"fit": "cover",
"opacity": 0.35
}
```
Video layers autoplay and loop. They are muted by default; set `muted` to `false` to
play their audio. WebM is recommended for size. Use an `opacity` below `1` if the video
should blend into the launcher UI.
#### Shape
```json
{
"id": "glow",
"type": "shape",
"shape": "ellipse",
"x": 680,
"y": 60,
"width": 260,
"height": 260,
"fill": "#ff006e",
"opacity": 0.18,
"borderWidth": 0,
"borderColor": "#ffffff"
}
```
`shape` is `rectangle`, `ellipse` or `line`. `radius` rounds rectangle corners.
`borderWidth` is clamped to `0`–`40`.
### Custom CSS
The `css` field is appended while the theme is active. Theme authors can use it for
small tweaks, but it is not required. Example:
```css
:root {
--radius: 1rem;
}
```
---
## Using the advanced editor
1. Open **Themes** in the launcher.
2. Select **Create theme**, or the pencil button on an installed API 2.0 theme.
3. The editor opens in its **own resizable window**, not a modal. The center is a
full launcher home canvas at 1000 × 650.
4. Use the left toolbar to add text, image, video and shape layers. Drag layers on
the canvas, resize with the corner handles, or edit exact values in the right
inspector. Shift/Cmd-click a layer or drag a marquee over empty canvas to
select several layers. Multi-selections can be dragged together and aligned
or distributed from the right panel. Layers snap to canvas edges, the canvas
center, and other layers while dragging; hold Alt to disable snapping.
Right-click a layer for quick duplicate, ordering, hide/lock and delete
actions, or right-click empty canvas to add a new layer.
5. The layer list supports double-click rename, hide/show, lock/unlock and
drag-to-reorder. Hidden layers remain in the package, while locked layers
cannot be moved or resized on the canvas.
6. Use the **Theme** tab for metadata, background, effects, typography and custom
CSS. Open the **Fonts** tab to search and download from every Fontsource
family, or import WOFF2, WOFF, TTF and OTF files. Downloaded fonts are copied
into the theme's `assets/` folder when you save.
7. Select **Save theme** to write `theme.json` and copy new assets into
`~/.ezpplauncher/themes`. A fresh `assets/preview.png` thumbnail is rendered
from the theme and the launcher applies the saved theme immediately. Unsaved
edits are autosaved as a local draft and offered for recovery after a crash.
8. Select **Export** to create a shareable `.ezpplauncher-theme` package. A theme
check runs first and reports missing assets, oversized media, missing fonts
and unused imports. Export saves and refreshes the preview, then reports the
final package size.
Keyboard shortcuts:
- `Cmd/Ctrl+Z` undoes the last change.
- `Cmd/Ctrl+Shift+Z` or `Ctrl+Y` redoes it.
- `Cmd/Ctrl+S` saves.
- `Cmd/Ctrl+D` duplicates the selected layer or multi-selection.
- `Cmd/Ctrl+A` selects every visible layer.
- Shift/Cmd-click toggles a layer in the current selection.
- Arrow keys nudge the selected layer(s) by 1 px; hold Shift for 10 px.
- `Delete` removes the selected layer(s).
- Hold Alt while dragging to skip snapping.
- `Esc` clears the selection or closes an open editor dialog.
---
## Creating API 2.0 themes with the CLI
The `create-ezpplauncher-theme` CLI includes a **declarative (API 2.0)** template:
```bash
npx git+https://git.ez-pp.farm/EZPPFarm/create-ezpplauncher-theme.git my-theme
```
Choose `declarative (API 2.0)`, edit `theme.json`, add files to `assets/`, then run:
```bash
npm run build
```
This produces `dist/my_theme.ezpplauncher-theme`. The template also includes
`preview.html` for a standalone browser preview:
```bash
npx serve .
```
---
## Compatibility and limits
- API 2.0 packages must include `theme.json`; `entry` and `style` are not used.
- Asset paths are resolved inside `assets/`; path traversal is rejected by the
import commands.
- Exports include only `assets/` files referenced by `theme.json` (background,
audio, fonts, layers and preview). Orphaned files left in the folder are
omitted, which keeps packages small. API 1.0 themes are exported whole because
their bundles can load files dynamically.
- The editor writes `preview.png` whenever you save or export. The generated
thumbnail renders the background, effects, launcher UI, theme fonts and
freeform layers; custom CSS is not applied to the thumbnail.
- The theme editor only writes inside `~/.ezpplauncher/themes`.
- API 1.0 themes are still loaded through their existing `mountTheme` bundle API.
- API 2.0 themes cannot run JavaScript. Use custom CSS for small visual tweaks, or
use API 1.0 when a theme needs a fully custom UI.
- Freeform layers are decorative, home-screen-only and pointer-transparent; they
cannot add interactive controls.
+353
View File
@@ -0,0 +1,353 @@
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
const windowManager = require('./ui/windowManager');
const osuUtil = require('./osuUtil');
const ezppUtil = require('./ezppUtil');
const config = require('./config');
const fs = require('fs');
const rpc = require('./discordPresence');
const windowName = require('get-window-by-name');
const terminalUtil = require('./terminalUtil');
const osUtil = require('./osUtil');
let tempOsuPath;
let osuWindowInfo;
let isIngame;
const platform = process.platform;
let linuxWMCtrlFound = false;
let osuLoaded = false;
const run = () => {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit();
return;
}
setInterval(async () => {
if (platform == "win32") {
osuWindowInfo = windowName.getWindowText("osu!.exe");
const firstInstance = osuWindowInfo[0];
if (firstInstance) {
if (firstInstance.processTitle && firstInstance.processTitle.length > 0) {
if (!osuLoaded) {
osuLoaded = true;
//TODO: do patch
}
const windowTitle = firstInstance.processTitle;
let rpcOsuVersion = "";
let currentMap = undefined;
if (!windowTitle.includes("-")) {
rpcOsuVersion = windowTitle;
rpc.updateState("Idle...");
} else {
var string = windowTitle;
var components = string.split(' - ');
const splitArray = [components.shift(), components.join(' - ')];
rpcOsuVersion = splitArray[0];
currentMap = splitArray[1];
if (currentMap.endsWith(".osu")) {
rpc.updateState("Editing...");
currentMap = currentMap.substring(0, currentMap.length - 4);
} else rpc.updateState("Playing...");
}
rpc.updateStatus(currentMap, rpcOsuVersion);
} else {
if (osuLoaded) osuLoaded = false;
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
} else {
if (osuLoaded) osuLoaded = false;
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
} else if (platform == "linux") {
if (!linuxWMCtrlFound) {
if (isIngame) {
rpc.updateState("Playing...");
rpc.updateStatus("Clicking circles!", "runningunderwine");
} else {
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
return;
}
const processesOutput = await terminalUtil.execCommand(`wmctrl -l|awk '{$3=""; $2=""; $1=""; print $0}'`);
const allLines = processesOutput.split("\n");
const filteredProcesses = allLines.filter((line) => line.trim().startsWith("osu!"));
if (filteredProcesses.length > 0) {
const windowTitle = filteredProcesses[0].trim();
let rpcOsuVersion = "";
let currentMap = undefined;
if (!windowTitle.includes("-")) {
rpcOsuVersion = windowTitle;
rpc.updateState("Idle...");
} else {
var string = windowTitle;
var components = string.split(' - ');
const splitArray = [components.shift(), components.join(' - ')];
rpcOsuVersion = splitArray[0];
currentMap = splitArray[1];
if (currentMap.endsWith(".osu")) {
rpc.updateState("Editing/Modding...");
currentMap = currentMap.substring(0, currentMap.length - 4);
}
else rpc.updateState("Playing...");
}
rpc.updateStatus(currentMap, rpcOsuVersion);
} else {
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
} else {
if (isIngame) {
rpc.updateState("Playing...");
rpc.updateStatus("Clicking circles!", "runningunderwine");
} else {
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
}
}, 2000);
setupTitlebar();
rpc.connect();
let mainWindow;
app.whenReady().then(() => {
mainWindow = createWindow();
mainWindow.on('show', async () => {
await updateConfigVars(mainWindow);
await tryLogin(mainWindow);
await doUpdateCheck(mainWindow);
if (platform === "linux") {
const linuxDistroInfo = await osUtil.getLinuxDistroInfo();
if (linuxDistroInfo?.id != "arch") {
if (linuxDistroInfo?.id_like != "arch") {
mainWindow.webContents.send('status_update', {
type: "info",
message: "We detected that you are running the Launcher under Linux. It's currently just compatible with Arch like distributions!"
});
}
}
try {
await terminalUtil.execCommand(`osu-stable -h`);
} catch (err) {
mainWindow.webContents.send('status_update', {
type: "package-issue",
message: "Seems like you dont have the osu AUR Package installed, please install it."
});
return;
}
/* mainWindow.webContents.send('status_update', {
type: "info",
message: "We detected that you are running the Launcher under Linux. It's currently just compatible with Arch and the osu AUR package!"
}); */
const terminalTest = await terminalUtil.execCommand(`wmctrl -l|awk '{$3=""; $2=""; $1=""; print $0}'`);
const isFailed = terminalTest.trim() == "";
if (isFailed) {
mainWindow.webContents.send('status_update', {
type: "info",
message: "Seems like you are missing the wmctrl package, please install it for the RPC to work!"
});
} else linuxWMCtrlFound = true;
} else {
const osuFolder = await config.get("osuPath");
if (!osuFolder || osuFolder == "") {
const foundFolder = await osuUtil.findOsuInstallation();
console.log("osu! Installation located at: ",foundFolder);
}
}
})
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) mainWindow = createWindow();
})
app.on('window-all-closed', () => {
app.quit()
})
ipcMain.handle('launch', async () => {
const osuConfig = await osuUtil.getLatestConfig(tempOsuPath);
const username = await config.get('username');
const password = await config.get('password');
if (password && username) {
await osuUtil.setConfigValue(osuConfig.path, "SaveUsername", "1");
await osuUtil.setConfigValue(osuConfig.path, "SavePassword", "1");
await osuUtil.setConfigValue(osuConfig.path, "Username", username);
await osuUtil.setConfigValue(osuConfig.path, "Password", password);
await osuUtil.setConfigValue(osuConfig.path, "CredentialEndpoint", "ez-pp.farm");
} else {
await osuUtil.setConfigValue(osuConfig.path, "Username", "");
await osuUtil.setConfigValue(osuConfig.path, "Password", "");
}
rpc.updateState("Launching osu!...");
isIngame = true;
mainWindow.hide();
const result = await osuUtil.startOsuWithDevServer(tempOsuPath, "ez-pp.farm", async () => {
isIngame = false;
mainWindow.show();
await doUpdateCheck(mainWindow);
});
return result;
})
ipcMain.on('do-update-check', async () => {
await doUpdateCheck(mainWindow);
})
ipcMain.on('do-update', async () => {
const osuPath = await config.get("osuPath", "");
const isValid = await osuUtil.isValidOsuFolder(osuPath);
if (osuPath.trim == "" || !isValid) {
mainWindow.webContents.send('status_update', {
type: "error",
message: "Invalid osu! folder"
});
return;
}
if (fs.existsSync(osuPath)) {
tempOsuPath = osuPath;
const osuConfig = await osuUtil.getLatestConfig(tempOsuPath);
const lastVersion = await osuConfig.get("LastVersion");
let releaseStream = "stable40";
if (lastVersion.endsWith("cuttingedge"))
releaseStream = "cuttingedge"
else if (lastVersion.endsWith("beta"))
releaseStream = "beta";
const releaseFiles = await osuUtil.getUpdateFiles(releaseStream);
const filesToDownload = await osuUtil.filesThatNeedUpdate(tempOsuPath, releaseFiles);
const downloadTask = await osuUtil.downloadUpdateFiles(osuPath, filesToDownload);
downloadTask.on('completed', () => {
mainWindow.webContents.send('status_update', {
type: "update-complete"
})
});
downloadTask.on('error', () => {
mainWindow.webContents.send('status_update', {
type: "error",
message: "An error occured while updating."
});
});
} else
mainWindow.webContents.send('status_update', {
type: "error",
message: "Invalid osu! folder"
});
})
ipcMain.handle('set-osu-dir', async (event) => {
const yes = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (yes.filePaths.length <= 0)
return undefined;
const folderPath = yes.filePaths[0];
const validOsuDir = await osuUtil.isValidOsuFolder(folderPath);
if (validOsuDir) await config.set("osuPath", folderPath);
return { validOsuDir, folderPath };
})
ipcMain.handle('perform-login', async (event, data) => {
const { username, password } = data;
const loginData = await ezppUtil.performLogin(username, password);
if (loginData && loginData.code === 200) {
await config.set("username", username);
await config.set("password", password);
}
return loginData;
})
ipcMain.on('perform-logout', async (event) => {
await config.remove("username");
await config.remove("password");
})
})
}
async function updateConfigVars(window) {
const osuPath = await config.get("osuPath", "");
window.webContents.send('config_update', {
osuPath: osuPath
})
}
async function tryLogin(window) {
const username = await config.get("username", "");
const password = await config.get("password", "");
if ((username && username.length > 0) && (password && password.length > 0)) {
const passwordPlain = password;
const loginResponse = await ezppUtil.performLogin(username, passwordPlain);
if (loginResponse && loginResponse.code === 200) {
window.webContents.send('account_update', {
type: "loggedin",
user: loginResponse.user
})
} else {
window.webContents.send('account_update', {
type: "login-failed"
})
}
} else {
window.webContents.send('account_update', {
type: "not-loggedin"
})
}
}
async function doUpdateCheck(window) {
const osuPath = await config.get("osuPath");
if (!osuPath || osuPath.trim == "") {
window.webContents.send('status_update', {
type: "missing-folder"
})
return;
}
const isValid = await osuUtil.isValidOsuFolder(osuPath);
if (!isValid) {
window.webContents.send('status_update', {
type: "missing-folder"
})
return;
}
if (fs.existsSync(osuPath)) {
tempOsuPath = osuPath;
const osuConfig = await osuUtil.getLatestConfig(tempOsuPath);
const lastVersion = await osuConfig.get("LastVersion");
let releaseStream = "stable40";
if (lastVersion.endsWith("cuttingedge"))
releaseStream = "cuttingedge"
else if (lastVersion.endsWith("beta"))
releaseStream = "beta";
const releaseFiles = await osuUtil.getUpdateFiles(releaseStream);
const filesToDownload = await osuUtil.filesThatNeedUpdate(tempOsuPath, releaseFiles);
window.webContents.send('status_update', {
type: filesToDownload.length > 0 ? "update-available" : "up-to-date"
})
} else
window.webContents.send('status_update', {
type: "missing-folder"
})
}
function createWindow() {
// Create the browser window.
const win = windowManager.createWindow(700, 460);
win.loadFile('./html/index.html');
attachTitlebarToWindow(win);
win.webContents.setWindowOpenHandler(() => "deny");
win.webContents.on('did-finish-load', function () {
if (win.webContents.getZoomFactor() != 0.9)
win.webContents.setZoomFactor(0.9)
});
return win;
}
run();
+4
View File
@@ -0,0 +1,4 @@
const appName = "EZPPLauncher"
const appVersion = "1.1.2";
module.exports = { appName, appVersion };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

+85
View File
@@ -0,0 +1,85 @@
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
position: absolute;
left: 0;
opacity: 0.01;
}
[type="checkbox"]:not(:checked)+label,
[type="checkbox"]:checked+label {
position: relative;
padding-left: 2.3em;
font-size: 1.05em;
line-height: 1.7;
cursor: pointer;
}
/* checkbox aspect */
[type="checkbox"]:not(:checked)+label:before,
[type="checkbox"]:checked+label:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 1.4em;
height: 1.4em;
border: 1px solid #565656;
background: #4c4c4c;
border-radius: .2em;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1), 0 0 0 hsla(var(--main-accent), 93%, 48%, 20%);
-webkit-transition: all .275s;
transition: all .275s;
}
/* checked mark aspect */
[type="checkbox"]:not(:checked)+label:after,
[type="checkbox"]:checked+label:after {
content: '✕';
position: absolute;
top: .525em;
left: .18em;
font-size: 1.375em;
font-weight: bolder;
color: hsl(var(--main-accent), 93%, 48%);
line-height: 0;
-webkit-transition: all .2s;
transition: all .2s;
}
/* checked mark aspect changes */
[type="checkbox"]:not(:checked)+label:after {
opacity: 0;
-webkit-transform: scale(0) rotate(45deg);
transform: scale(0) rotate(45deg);
}
[type="checkbox"]:checked+label:after {
opacity: 1;
-webkit-transform: scale(1) rotate(0);
transform: scale(1) rotate(0);
translate: -1.5px -1px;
}
/* Disabled checkbox */
[type="checkbox"]:disabled:not(:checked)+label:before,
[type="checkbox"]:disabled:checked+label:before {
box-shadow: none;
border-color: #565656;
background-color: #4c4c4c;
cursor: not-allowed;
}
[type="checkbox"]:disabled:checked+label:after {
color: #777;
}
[type="checkbox"]:disabled+label {
color: #aaa;
cursor: not-allowed;
}
/* Accessibility */
[type="checkbox"]:checked:focus+label:before,
[type="checkbox"]:not(:checked):focus+label:before {
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1), 0 0 0 6px hsla(var(--main-accent), 93%, 48%, 20%);
}
+322
View File
@@ -0,0 +1,322 @@
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
:root {
--main-accent: 335deg;
--main-bg: 230deg;
}
* {
font-family: 'Exo 2', 'Roboto' !important;
}
body {
background-color: hsl(var(--main-bg), 24%, 19%);
}
.sections {
display: flex;
flex-flow: column;
}
.app-name {
font-size: 40px;
font-weight: 500;
text-shadow: 0 0 15px rgba(255, 255, 255, 0.37);
}
.launcher-window {
width: 100%;
}
.launch-button-section {
display: flex;
flex-direction: column;
}
.launch-button-section button {
margin-bottom: 10px;
}
.loading-section,
.login-section {
margin-top: 35px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
.loading-section {
margin-top: 0;
}
.loading-section {}
.loading-indicator {}
.loading-indicator-text {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
.folder-section {
margin-top: 50px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
color: #aaa;
font-weight: 500;
}
.folder-action {
font-style: italic;
cursor: pointer;
width: fit-content;
line-height: .9;
}
.launch-section {
width: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
.btn.btn-launch {
margin-top: 10px;
width: 300px;
font-size: 23px;
text-transform: none;
}
.account-section {
display: flex;
align-items: center;
gap: 20px;
}
.server-logo img {
user-select: none;
pointer-events: none;
}
.user-info {
text-align: start;
display: flex;
flex-flow: column;
justify-content: center;
align-content: center;
overflow: hidden;
text-overflow: ellipsis;
}
.user-info .welcome-text {
font-size: 31px;
letter-spacing: .02rem;
height: 30px;
line-height: .8;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.349);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-info .account-action {
font-size: 21px;
letter-spacing: .05rem;
font-weight: 500;
font-style: italic;
color: #aaa;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.349);
cursor: pointer;
line-height: 1.2;
width: fit-content;
}
.user-image {
border: 5px solid white;
border-radius: 0.4rem;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.349);
background-image: url(https://a.ez-pp.farm/0);
width: 100px;
height: 100px;
background-size: cover;
background-position: center;
}
.form-outline .form-control:focus~.form-notch .form-notch-leading {
border-color: hsl(var(--main-accent), 93%, 48%);
box-shadow: -1px 0 0 0 hsl(var(--main-accent), 93%, 48%), 0 1px 0 0 hsl(var(--main-accent), 93%, 48%), 0 -1px 0 0 hsl(var(--main-accent), 93%, 48%);
}
.form-outline .form-control:focus~.form-notch .form-notch-middle {
border-color: hsl(var(--main-accent), 93%, 48%);
box-shadow: 0 1px 0 0 hsl(var(--main-accent), 93%, 48%);
border-top: 1px solid transparent;
}
.form-outline .form-control:focus~.form-notch .form-notch-trailing {
border-color: hsl(var(--main-accent), 93%, 48%);
box-shadow: 1px 0 0 0 hsl(var(--main-accent), 93%, 48%), 0 -1px 0 0 hsl(var(--main-accent), 93%, 48%), 0 1px 0 0 hsl(var(--main-accent), 93%, 48%);
}
.form-outline .form-control:focus~.form-label {
color: hsl(var(--main-accent), 93%, 48%);
}
.btn-accent,
.btn-accent:active {
background: hsl(var(--main-accent), 93%, 48%);
color: #fff;
}
.btn-accent:hover {
background: hsl(var(--main-accent), 93%, 42%);
color: #fff ;
}
.btn-grey,
.btn-grey:active {
background: #727272;
color: #fff;
}
.btn-grey:hover {
background: #626262;
color: #fff;
}
.btn-grouped {
display: flex;
gap: 50px;
}
.clickable {
cursor: pointer;
}
.loader {
position: relative;
margin: 0px auto;
width: 200px;
height: 200px;
transform: scale(0.5);
}
.loader:before {
content: '';
display: block;
padding-top: 100%;
}
.circular-loader {
-webkit-animation: rotate 2s linear infinite;
animation: rotate 2s linear infinite;
height: 100%;
-webkit-transform-origin: center center;
-ms-transform-origin: center center;
transform-origin: center center;
width: 100%;
position: absolute;
top: 0;
left: 0;
margin: auto;
}
.loader-path {
stroke-dasharray: 150, 200;
stroke-dashoffset: -10;
-webkit-animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
stroke-linecap: round;
}
@-webkit-keyframes rotate {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotate {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124;
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124;
}
}
@-webkit-keyframes color {
0% {
stroke: #fff;
}
40% {
stroke: #fff;
}
66% {
stroke: #fff;
}
80%,
90% {
stroke: #fff;
}
}
@keyframes color {
0% {
stroke: #fff;
}
40% {
stroke: #fff;
}
66% {
stroke: #fff;
}
80%,
90% {
stroke: #fff;
}
}
File renamed without changes.
+23
View File
@@ -0,0 +1,23 @@
:root{--mdb-blue:#0d6efd;--mdb-indigo:#6610f2;--mdb-purple:#6f42c1;--mdb-pink:#d63384;--mdb-red:#dc3545;--mdb-orange:#fd7e14;--mdb-yellow:#ffc107;--mdb-green:#198754;--mdb-teal:#20c997;--mdb-cyan:#0dcaf0;--mdb-gray:#757575;--mdb-gray-dark:#4f4f4f;--mdb-gray-100:#f5f5f5;--mdb-gray-200:#eee;--mdb-gray-300:#e0e0e0;--mdb-gray-400:#bdbdbd;--mdb-gray-500:#9e9e9e;--mdb-gray-600:#757575;--mdb-gray-700:#616161;--mdb-gray-800:#4f4f4f;--mdb-gray-900:#262626;--mdb-primary:#1266f1;--mdb-secondary:#b23cfd;--mdb-success:#00b74a;--mdb-info:#39c0ed;--mdb-warning:#ffa900;--mdb-danger:#f93154;--mdb-light:#f9f9f9;--mdb-dark:#262626;--mdb-white:#fff;--mdb-black:#000;--mdb-primary-rgb:18,102,241;--mdb-secondary-rgb:178,60,253;--mdb-success-rgb:0,183,74;--mdb-info-rgb:57,192,237;--mdb-warning-rgb:255,169,0;--mdb-danger-rgb:249,49,84;--mdb-light-rgb:249,249,249;--mdb-dark-rgb:38,38,38;--mdb-white-rgb:255,255,255;--mdb-black-rgb:0,0,0;--mdb-body-color-rgb:79,79,79;--mdb-body-bg-rgb:255,255,255;--mdb-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--mdb-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--mdb-gradient:linear-gradient(180deg,hsla(0,0%,100%,0.15),hsla(0,0%,100%,0));--mdb-body-font-family:var(--mdb-font-roboto);--mdb-body-font-size:1rem;--mdb-body-font-weight:400;--mdb-body-line-height:1.6;--mdb-body-color:#4f4f4f;--mdb-body-bg:#fff}*,:after,:before{box-sizing:border-box}@media(prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--mdb-body-font-family);font-size:var(--mdb-body-font-size);font-weight:var(--mdb-body-font-weight);line-height:var(--mdb-body-line-height);color:var(--mdb-body-color);text-align:var(--mdb-body-text-align);background-color:var(--mdb-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media(min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media(min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media(min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media(min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-mdb-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#1266f1;text-decoration:underline}a:hover{color:#0e52c1}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--mdb-font-monospace);font-size:1em;/*!rtl:ignore*/direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#262626;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:1rem;padding-bottom:1rem;color:#757575;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border:0 solid;border-color:inherit}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;foLine truncated
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width:1200px){.display-6{font-size:2.5rem}}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#757575}.blockquote-footer:before{content:"— "}.img-fluid,.img-thumbnail{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #e0e0e0;border-radius:.25rem}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#757575}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--mdb-gutter-x,.75rem);padding-left:var(--mdb-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media(min-width:576px){.container,.container-sm{max-width:540px}}@media(min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media(min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media(min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media(min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--mdb-gutter-x:1.5rem;--mdb-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--mdb-gutter-y)*-1);margin-right:calc(var(--mdb-gutter-x)*-0.5);margin-left:calc(var(--mdb-gutter-x)*-0.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--mdb-gutter-x)*0.5);padding-left:calc(var(--mdb-gutter-x)*0.5);margin-top:var(--mdb-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--mdb-gutter-x:0}.g-0,.gy-0{--mdb-gutter-y:0}.g-1,.gx-1{--mdb-gutter-x:0.25rem}.g-1,.gy-1{--mdb-gutter-y:0.25rem}.g-2,.gx-2{--mdb-gutter-x:0.5rem}.g-2,.gy-2{--mdb-gutter-y:0.5rem}.g-3,.gx-3{--mdb-gutter-x:1rem}.g-3,.gy-3{--mdb-gutter-y:1rem}.g-4,.gx-4{--mdb-gutter-x:1.5rem}.g-4,.gy-4{--mdb-gutter-y:1.5rem}.g-5,.gx-5{--mdb-gutter-x:3rem}.g-5,.gy-5{--mdb-gutter-y:3rem}@media(min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;widthLine truncated
"autoRename": true,
"stringMap":[ {
"name" : "prev-next",
"search" : "prev",
"replace" : "next"
} ]
}*/.carousel-control-next-icon,.carousel-control-prev-icon{background-image:none}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-mdb-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion:reduce){.carousel-indicators [data-mdb-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-mdb-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{/*!rtl:ignore*/to{transform:rotate(1turn)}}@keyframes spinner-border{/*!rtl:ignore*/to{transform:rotate(1turn)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid;border-right:.25em solid transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}@media(prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media(prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem}.offcanvas-header .btn-close{padding:.5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.6}.offcanvas-body{flex-grow:1;padding:1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom,.offcanvas-top{right:0;left:0;height:30vh;max-height:100%}.offcanvas-bottom{border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.tooltip{position:absolute;z-index:1090;display:block;margin:0;font-family:var(--mdb-font-roboto);font-style:normal;font-weight:400;line-height:1.6;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow:before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow:before,.bs-tooltip-top .tooltip-arrow:before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow:before,.bs-tooltip-end .tooltip-arrow:before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow:before,.bs-tooltip-bottom .tooltip-arrow:bLine truncated
* # Semantic UI 2.4.2 - Flag
* http://github.com/semantic-org/semantic-ui/
*
*
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/#mdb-table-flag tr{cursor:pointer}.mdb-flag-selected{border-top-left-radius:5px;border-top-right-radius:5px;text-align:center;max-width:150px;margin:10px auto 0}.mdb-selected-flag-text{margin:0 auto;max-width:150px}i.flag:not(.icon){margin:0 .5em 0 0;line-height:11px;text-decoration:inherit;vertical-align:baseline;-webkit-backface-visibility:hidden;backface-visibility:hidden}i.flag:before,i.flag:not(.icon){display:inline-block;width:16px;height:11px}i.flag:before{content:"";background:url(https://mdbootstrap.com/img/svg/flags.png) no-repeat -108px -1976px}i.flag-ad:before,i.flag-andorra:before{background-position:0 0!important}i.flag-ae:before,i.flag-uae:before,i.flag-united-arab-emirates:before{background-position:0 -26px!important}i.flag-af:before,i.flag-afghanistan:before{background-position:0 -52px!important}i.flag-ag:before,i.flag-antigua:before{background-position:0 -78px!important}i.flag-ai:before,i.flag-anguilla:before{background-position:0 -104px!important}i.flag-al:before,i.flag-albania:before{background-position:0 -130px!important}i.flag-am:before,i.flag-armenia:before{background-position:0 -156px!important}i.flag-an:before,i.flag-netherlands-antilles:before{background-position:0 -182px!important}i.flag-angola:before,i.flag-ao:before{background-position:0 -208px!important}i.flag-ar:before,i.flag-argentina:before{background-position:0 -234px!important}i.flag-american-samoa:before,i.flag-as:before{background-position:0 -260px!important}i.flag-at:before,i.flag-austria:before{background-position:0 -286px!important}i.flag-au:before,i.flag-australia:before{background-position:0 -312px!important}i.flag-aruba:before,i.flag-aw:before{background-position:0 -338px!important}i.flag-aland-islands:before,i.flag-ax:before{background-position:0 -364px!important}i.flag-az:before,i.flag-azerbaijan:before{background-position:0 -390px!important}i.flag-ba:before,i.flag-bosnia:before{background-position:0 -416px!important}i.flag-barbados:before,i.flag-bb:before{background-position:0 -442px!important}i.flag-bangladesh:before,i.flag-bd:before{background-position:0 -468px!important}i.flag-be:before,i.flag-belgium:before{background-position:0 -494px!important}i.flag-bf:before,i.flag-burkina-faso:before{background-position:0 -520px!important}i.flag-bg:before,i.flag-bulgaria:before{background-position:0 -546px!important}i.flag-bahrain:before,i.flag-bh:before{background-position:0 -572px!important}i.flag-bi:before,i.flag-burundi:before{background-position:0 -598px!important}i.flag-benin:before,i.flag-bj:before{background-position:0 -624px!important}i.flag-bermuda:before,i.flag-bm:before{background-position:0 -650px!important}i.flag-bn:before,i.flag-brunei:before{background-position:0 -676px!important}i.flag-bo:before,i.flag-bolivia:before{background-position:0 -702px!important}i.flag-br:before,i.flag-brazil:before{background-position:0 -728px!important}i.flag-bahamas:before,i.flag-bs:before{background-position:0 -754px!important}i.flag-bhutan:before,i.flag-bt:before{background-position:0 -780px!important}i.flag-bouvet-island:before,i.flag-bv:before{background-position:0 -806px!important}i.flag-botswana:before,i.flag-bw:before{background-position:0 -832px!important}i.flag-belarus:before,i.flag-by:before{background-position:0 -858px!important}i.flag-belize:before,i.flag-bz:before{background-position:0 -884px!important}i.flag-ca:before,i.flag-canada:before{background-position:0 -910px!important}i.flag-cc:before,i.flag-cocos-islands:before{background-position:0 -962px!important}i.flag-cd:before,i.flag-congo:before{background-position:0 -988px!important}i.flag-central-african-republic:before,i.flag-cf:before{background-position:0 -1014px!important}i.flag-cg:before,i.flag-congo-brazzaville:before{background-position:0 -1040px!important}i.flag-ch:before,i.flag-switzerland:before{background-position:0 -1066px!important}i.flag-ci:before,i.flag-cote-divoire:before{background-position:0 -1092px!important}i.flag-ck:before,i.flag-cook-islands:before{background-position:0 -1118px!important}i.flag-chile:before,i.flag-cl:before{background-position:0 -1144px!important}i.flag-cameroon:before,i.flag-cm:before{background-position:0 -1170px!important}i.flag-china:before,i.flag-cn:before{background-position:0 -1196px!important}i.flag-co:before,i.flag-colombia:before{background-position:0 -1222px!important}i.flag-costa-rica:before,i.flag-cr:before{background-position:0 -1248px!important}i.flag-cs:before,i.flag-serbia:before{background-position:0 -1274px!important}i.flag-cu:before,i.flag-cuba:before{background-position:0 -1300px!important}i.flag-cape-verde:before,i.flag-cv:before{background-position:0 -1326px!important}i.flag-christmas-island:before,i.flag-cx:before{background-position:0 -1352px!important}i.flag-cy:before,i.flag-cyprus:before{background-position:0 -1378px!important}i.flag-cz:before,i.flag-czech-republic:before{background-position:0 -1404px!important}i.flag-de:before,i.flag-germany:before{background-position:0 -1430px!important}Line truncated
+20
View File
@@ -0,0 +1,20 @@
/*!
* MDB5
* Version: FREE 4.2.0
*
*
* Copyright: Material Design for Bootstrap
* https://mdbootstrap.com/
*
* Read the license: https://mdbootstrap.com/general/license/
*
*
* Documentation: https://mdbootstrap.com/docs/standard/
*
* Support: https://mdbootstrap.com/support/
*
* Contact: [email protected]
*
*/
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("mdb",[],e):"object"==typeof exports?exports.mdb=e():t.mdb=e()}(this,function(){return n=[function(t,e){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,e,n){"use strict";var i=n(18),n=n(45);i({target:"RegExp",proto:!0,forced:/./.exec!==n},{exec:n})},function(t,e,n){var n=n(33),i=Function.prototype,o=i.bind,r=i.call,s=n&&o.bind(r,r);t.exports=n?function(t){return t&&s(t)}:function(t){return t&&function(){return r.apply(t,arguments)}}},function(t,e){t.exports=function(t){return"function"==typeof t}},function(n,t,e){!function(t){function e(t){return t&&t.Math==Math&&t}n.exports=e("object"==typeof globalThis&&globalThis)||e("object"==typeof window&&window)||e("object"==typeof self&&self)||e("object"==typeof t&&t)||function(){return this}()||Function("return this")()}.call(this,e(75))},function(t,e,n){var i=n(4),o=n(36),r=n(7),s=n(55),a=n(50),c=n(49),l=o("wks"),u=i.Symbol,h=u&&u.for,d=c?u:u&&u.withoutSetter||s;t.exports=function(t){var e;return r(l,t)&&(a||"string"==typeof l[t])||(e="Symbol."+t,a&&r(u,t)?l[t]=u[t]:l[t]=(c&&h?h:d)(e)),l[t]}},function(t,e,n){n=n(0);t.exports=!n(function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})},function(t,e,n){var i=n(2),o=n(28),r=i({}.hasOwnProperty);t.exports=Object.hasOwn||function(t,e){return r(o(t),e)}},function(t,e,n){"use strict";var i=n(18),o=n(61).includes,r=n(0),n=n(71);i({target:"Array",proto:!0,forced:r(function(){return!Array(1).includes()})},{includes:function(t){return o(this,t,1<arguments.length?arguments[1]:void 0)}}),n("includes")},function(t,e,n){var i=n(6),o=n(56),r=n(57),s=n(12),a=n(34),c=TypeError,l=Object.defineProperty,u=Object.getOwnPropertyDescriptor,h="enumerable",d="configurable",f="writable";e.f=i?r?function(t,e,n){var i;return s(t),e=a(e),s(n),"function"==typeof t&&"prototype"===e&&"value"in n&&f in n&&!n[f]&&((i=u(t,e))&&i[f]&&(t[e]=n.value,n={configurable:(d in n?n:i)[d],enumerable:(h in n?n:i)[h],writable:!1})),l(t,e,n)}:l:function(t,e,n){if(s(t),e=a(e),s(n),o)try{return l(t,e,n)}catch(t){}if("get"in n||"set"in n)throw c("Accessors not supported");return"value"in n&&(t[e]=n.value),t}},function(t,e,n){"use strict";var i=n(19),o=n(71),r=n(46),s=n(29),a=n(9).f,c=n(105),l=n(27),n=n(6),u="Array Iterator",h=s.set,d=s.getterFor(u),s=(t.exports=c(Array,"Array",function(t,e){h(this,{type:u,target:i(t),index:0,kind:e})},function(){var t=d(this),e=t.target,n=t.kind,i=t.index++;return!e||i>=e.length?{value:t.target=void 0,done:!0}:"keys"==n?{value:i,done:!1}:"values"==n?{value:e[i],done:!1}:{value:[i,e[i]],done:!1}},"values"),r.Arguments=r.Array);if(o("keys"),o("values"),o("entries"),!l&&n&&"values"!==s.name)try{a(s,"name",{value:"values"})}catch(t){}},function(t,e,n){var n=n(33),i=Function.prototype.call;t.exports=n?i.bind(i):function(){return i.apply(i,arguments)}},function(t,e,n){var i=n(14),o=String,r=TypeError;t.exports=function(t){if(i(t))return t;throw r(o(t)+" is not an object")}},function(t,e,n){function i(e,t){if(e){if(e[u]!==d)try{l(e,u,d)}catch(t){e[u]=d}if(e[h]||l(e,h,t),s[t])for(var n in c)if(e[n]!==c[n])try{l(e,n,c[n])}catch(t){e[n]=c[n]}}}var o,r=n(4),s=n(108),a=n(109),c=n(10),l=n(15),n=n(5),u=n("iterator"),h=n("toStringTag"),d=c.values;for(o in s)i(r[o]&&r[o].prototype,o);i(a,"DOMTokenList")},function(t,e,n){var i=n(3);t.exports=function(t){return"object"==typeof t?null!==t:i(t)}},function(t,e,n){var i=n(6),o=n(9),r=n(24);t.exports=i?function(t,e,n){return o.f(t,e,r(1,n))}:function(t,e,n){return t[e]=n,t}},function(t,e,n){var i=n(86),o=String;t.exports=function(t){if("Symbol"===i(t))throw TypeError("Cannot convert a Symbol value to a string");return o(t)}},function(M,H,t){var e=t(6),n=t(4),i=t(2),o=t(64),l=t(93),u=t(15),r=t(59).f,h=t(35),d=t(95),f=t(16),p=t(96),s=t(66),a=t(97),c=t(22),g=t(0),m=t(7),_=t(29).enforce,v=t(98),b=t(5),y=t(67),w=t(68),E=b("match"),x=n.RegExp,C=x.prototype,T=n.SyntaxError,O=i(C.exec),A=i("".charAt),S=i("".replace),L=i("".indexOf),R=i("".slice),B=/^\?<[^\s\d!#%&*+<=>@^][^\s!#%&*+<=>@^]*>/,I=/a/g,k=/a/g,t=new x(I)!==I,D=s.MISSED_STICKY,W=s.UNSUPPORTED_Y,b=e&&(!t||D||y||w||g(function(){return k[E]=!1,x(I)!=I||x(k)==k||"/a/i"!=x(I,"i")}));if(o("RegExp",b)){function N(t,e){var n,i,o=h(C,this),r=d(t),s=void 0===e,a=[],c=t;if(!o&&r&&s&&t.constructor===N)return t;if((r||h(C,t))&&(t=t.source,s&&(e=p(c))),t=void 0===t?"":f(t),e=void 0===e?"":f(e),c=t,r=e=y&&"dotAll"in I&&(n=!!e&&-1<L(e,"s"))?S(e,/s/g,""):e,D&&"sticky"in I&&(i=!!e&&-1<L(e,"y"))&&W&&(e=S(e,/y/g,"")),w&&(t=(s=function(t){for(var e,n=t.length,i=0,o="",r=[],s={},a=!1,c=!1,l=0,u="";i<=n;i++){if("\\"===(e=A(t,i)))e+=A(t,++i);else if("]"===e)a=!1;else if(!a)switch(!0){case"["===e:a=!0;break;case"("===e:O(B,R(t,i+1))&&(i+=2,c=!0),o+=e,l++;continue;case">"===e&&c:if(""===u||m(s,u))throw new T("Invalid capture group name");s[u]=!0,c=!(r[r.length]=[u,l]),u="";continue}c?u+=e:o+=e}return[o,r]}(Line truncated
//# sourceMappingURL=mdb.min.js.map
File diff suppressed because it is too large. Load diff
-845
View File
@@ -1,845 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "ezpplauncher",
"dependencies": {
"@better-fetch/fetch": "^1.1.21",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@fontsource-variable/montserrat": "^5.3.0",
"@fontsource-variable/playfair-display": "^5.3.0",
"@fontsource-variable/sora": "^5.2.8",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/space-mono": "^5.2.9",
"@number-flow/svelte": "^0.4.0",
"@tailwindcss/vite": "^4.3.0",
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@types/node": "^25.9.1",
"animejs": "^4.4.1",
"buffer": "^6.0.3",
"crypto-js": "^4.2.0",
"jszip": "^3.10.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"semver": "^7.8.1",
"sileo": "^0.1.5",
"svelte-confetti": "^2.3.2",
"torph": "^0.1.3",
"tw-animate-css": "^1.4.0",
},
"devDependencies": {
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@iarna/toml": "2.2.5",
"@lucide/svelte": "^1.16.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.61.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tauri-apps/cli": "^2.11.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/bun": "^1.3.14",
"@types/crypto-js": "^4.2.2",
"@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1",
"bits-ui": "^2.18.1",
"clsx": "^2.1.1",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.17.1",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^4.0.1",
"sass-embedded": "^1.100.0",
"svelte": "^5.55.9",
"svelte-check": "^4.4.8",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.3.0",
"tslib": "^2.8.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.0",
"vite": "^8.0.14",
"vite-plugin-devtools-json": "^1.0.0",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
"@babel/helper-globals": ["@babel/[email protected]", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-string-parser": ["@babel/[email protected]", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/parser": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
"@better-fetch/fetch": ["@better-fetch/[email protected]", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
"@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-foZ7qr0IsUBjzWIq+SuBLfdQCpJ1j8cTuNNT4owngTHoN5KsJb8L9t65fzz7SCeSWzescoOil/0ldqiL041ABg=="],
"@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/wasi-threads": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@eslint-community/eslint-utils": ["@eslint-community/[email protected]", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/[email protected]", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/compat": ["@eslint/[email protected]", "", { "dependencies": { "@eslint/core": "^1.2.1" }, "peerDependencies": { "eslint": "^8.40 || 9 || 10" }, "optionalPeers": ["eslint"] }, "sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g=="],
"@eslint/config-array": ["@eslint/[email protected]", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
"@eslint/config-helpers": ["@eslint/[email protected]", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
"@eslint/core": ["@eslint/[email protected]", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/js": ["@eslint/[email protected]", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
"@eslint/object-schema": ["@eslint/[email protected]", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
"@eslint/plugin-kit": ["@eslint/[email protected]", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
"@floating-ui/core": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="],
"@floating-ui/dom": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="],
"@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
"@fontsource-variable/jetbrains-mono": ["@fontsource-variable/[email protected]", "", {}, "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="],
"@fontsource-variable/montserrat": ["@fontsource-variable/[email protected]", "", {}, "sha512-7PaZoxaxrWLAyrhO46v65An9LhUhfkTExWLhfbywYZCnZEgg/W1rEHnlNmZKjNZ3nJTVYyicqxlJt10z/26yTA=="],
"@fontsource-variable/playfair-display": ["@fontsource-variable/[email protected]", "", {}, "sha512-IHdzTvE8eGIXHteZql7KXoqdc3Gfm+Zs1u4XlSC3MeplkwviT3sx/P6gI0tLwg/xIYLF4ZJYveZ5V+bl9cnDwA=="],
"@fontsource-variable/sora": ["@fontsource-variable/[email protected]", "", {}, "sha512-sP+ILTfi5r3cqmMBntI4ooQNxFI+LyHyD5hcPbWwwL4uh4kKFAkOz0w2veCXjz18fKRdva6OG0dY+5yQ02f30w=="],
"@fontsource/bebas-neue": ["@fontsource/[email protected]", "", {}, "sha512-nCxMEb9Bg9sjNWm9TIOsvHKDMLRUfEZPWpLgYu6m9KavZZVSA/WoHzI5wROwqA+eNwEi/PNBAcB5Co7HA3S0Cw=="],
"@fontsource/space-mono": ["@fontsource/[email protected]", "", {}, "sha512-b61faFOHEISQ/pD25G+cfGY9o/WW6lRv6hBQQfpWvEJ4y1V+S4gmth95EVyBE2VL3qDYHeVQ8nBzrplzdXTDDg=="],
"@humanfs/core": ["@humanfs/[email protected]", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/[email protected]", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/[email protected]", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/[email protected]", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@iarna/toml": ["@iarna/[email protected]", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="],
"@internationalized/date": ["@internationalized/[email protected]", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA=="],
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/[email protected]", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@lucide/svelte": ["@lucide/[email protected]", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-AvvPJnaWxeiNkAljI5MsSEc84yHPLMaWQIAJOcbX7k9au/f9ITS7cxTTQiautDiOFKVOXiYdZ+d6mtl88J+Kbg=="],
"@napi-rs/wasm-runtime": ["@napi-rs/[email protected]", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@number-flow/svelte": ["@number-flow/[email protected]", "", { "dependencies": { "esm-env": "^1.1.4", "number-flow": "0.6.0" }, "peerDependencies": { "svelte": "^4 || ^5" } }, "sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA=="],
"@oxc-project/types": ["@oxc-project/[email protected]", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
"@parcel/watcher": ["@parcel/[email protected]", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
"@parcel/watcher-android-arm64": ["@parcel/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
"@parcel/watcher-darwin-arm64": ["@parcel/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
"@parcel/watcher-darwin-x64": ["@parcel/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
"@parcel/watcher-freebsd-x64": ["@parcel/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
"@parcel/watcher-linux-arm-glibc": ["@parcel/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
"@parcel/watcher-linux-arm-musl": ["@parcel/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
"@parcel/watcher-linux-arm64-glibc": ["@parcel/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
"@parcel/watcher-linux-arm64-musl": ["@parcel/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
"@parcel/watcher-linux-x64-glibc": ["@parcel/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
"@parcel/watcher-linux-x64-musl": ["@parcel/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
"@parcel/watcher-win32-arm64": ["@parcel/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
"@parcel/watcher-win32-ia32": ["@parcel/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
"@parcel/watcher-win32-x64": ["@parcel/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
"@polka/url": ["@polka/[email protected]", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rolldown/binding-android-arm64": ["@rolldown/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="],
"@rolldown/binding-darwin-x64": ["@rolldown/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/[email protected]", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
"@rolldown/pluginutils": ["@rolldown/[email protected]", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@sveltejs/acorn-typescript": ["@sveltejs/[email protected]", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="],
"@sveltejs/adapter-static": ["@sveltejs/[email protected]", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
"@sveltejs/kit": ["@sveltejs/[email protected]", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/[email protected]", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA=="],
"@swc/helpers": ["@swc/[email protected]", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
"@tailwindcss/node": ["@tailwindcss/[email protected]", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
"@tailwindcss/oxide": ["@tailwindcss/[email protected]", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/[email protected]", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
"@tailwindcss/vite": ["@tailwindcss/[email protected]", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="],
"@tauri-apps/api": ["@tauri-apps/[email protected]", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="],
"@tauri-apps/cli": ["@tauri-apps/[email protected]", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw=="],
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ=="],
"@tauri-apps/plugin-fs": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ=="],
"@tauri-apps/plugin-shell": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="],
"@trivago/prettier-plugin-sort-imports": ["@trivago/[email protected]", "", { "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "javascript-natural-sort": "^0.7.1", "lodash-es": "^4.17.21", "minimatch": "^9.0.0", "parse-imports-exports": "^0.2.4" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-ember-template-tag": ">= 2.0.0", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-ember-template-tag", "prettier-plugin-svelte", "svelte"] }, "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA=="],
"@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/cookie": ["@types/[email protected]", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/crypto-js": ["@types/[email protected]", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="],
"@types/esrecurse": ["@types/[email protected]", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/semver": ["@types/[email protected]", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="],
"@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/type-utils": "8.60.0", "@typescript-eslint/utils": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw=="],
"@typescript-eslint/parser": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.0", "@typescript-eslint/types": "^8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/[email protected]", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg=="],
"@typescript-eslint/types": ["@typescript-eslint/[email protected]", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.0", "@typescript-eslint/tsconfig-utils": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g=="],
"@typescript-eslint/utils": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="],
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["[email protected]", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"animejs": ["[email protected]", "", {}, "sha512-XimhJw4vPb6tAd6Zaoa6BaspJ8UteQMKvL7Pvrt6MW/6u2UvJs0TB/LLqR1sigaiYvpVPC3Tokr9emcCMhWFhw=="],
"aria-query": ["[email protected]", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"axobject-query": ["[email protected]", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"balanced-match": ["[email protected]", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["[email protected]", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bits-ui": ["[email protected]", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA=="],
"brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"braces": ["[email protected]", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"colorjs.io": ["[email protected]", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
"cookie": ["[email protected]", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"core-util-is": ["[email protected]", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cross-spawn": ["[email protected]", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crypto-js": ["[email protected]", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
"cssesc": ["[email protected]", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-is": ["[email protected]", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge": ["[email protected]", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"dequal": ["[email protected]", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["[email protected]", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"devalue": ["[email protected]", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
"enhanced-resolve": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
"escape-string-regexp": ["[email protected]", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["[email protected]", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="],
"eslint-config-prettier": ["[email protected]", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
"eslint-plugin-svelte": ["[email protected]", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ=="],
"eslint-scope": ["[email protected]", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["[email protected]", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"esm-env": ["[email protected]", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"espree": ["[email protected]", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"esquery": ["[email protected]", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrap": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A=="],
"esrecurse": ["[email protected]", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["[email protected]", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["[email protected]", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["[email protected]", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["[email protected]", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["[email protected]", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["[email protected]", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["[email protected]", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["[email protected]", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["[email protected]", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.34.1", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-kcZyNaYQfvE2LlH6+AyOaJAQV4rGp5XbzfhsZpiSZcwDMfZUHhuxLWeyRzf5I7jip3qKRpuimPA9pXXfr111kQ=="],
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["[email protected]", "", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="],
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["[email protected]", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"immediate": ["[email protected]", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"immutable": ["[email protected]", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="],
"imurmurhash": ["[email protected]", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"inline-style-parser": ["[email protected]", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
"is-extglob": ["[email protected]", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["[email protected]", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["[email protected]", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-reference": ["[email protected]", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"isarray": ["[email protected]", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"isexe": ["[email protected]", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"javascript-natural-sort": ["[email protected]", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="],
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["[email protected]", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["[email protected]", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["[email protected]", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"jszip": ["[email protected]", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
"keyv": ["[email protected]", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kleur": ["[email protected]", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"known-css-properties": ["[email protected]", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
"levn": ["[email protected]", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lie": ["[email protected]", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"lightningcss": ["[email protected]", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lilconfig": ["[email protected]", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"locate-character": ["[email protected]", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"locate-path": ["[email protected]", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash-es": ["[email protected]", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lz-string": ["[email protected]", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"micromatch": ["[email protected]", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.34.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N9RVNGn/NSo85OgHX1wGaUWHvReuQ7dZUwuQRhHyzY2wfVOvY3cEgn0Mw4NXOsXMHL/y7EYuzA+b59PYI6EejA=="],
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-SC7ZC5dRcGwku2g7EsPvI4q/EzHumUbqsDNumBmZTLFg+goBO5LTJvDu9MAxx+0mtX4IA78B2be/A3aRjY0jnw=="],
"motion-utils": ["[email protected]", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
"mri": ["[email protected]", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"natural-compare": ["[email protected]", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-addon-api": ["[email protected]", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"number-flow": ["[email protected]", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ=="],
"obug": ["[email protected]", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"optionator": ["[email protected]", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["[email protected]", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["[email protected]", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"pako": ["[email protected]", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"parse-imports-exports": ["[email protected]", "", { "dependencies": { "parse-statements": "1.0.11" } }, "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ=="],
"parse-statements": ["[email protected]", "", {}, "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA=="],
"path-exists": ["[email protected]", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["[email protected]", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["[email protected]", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"postcss-load-config": ["[email protected]", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
"postcss-safe-parser": ["[email protected]", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
"postcss-scss": ["[email protected]", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
"postcss-selector-parser": ["[email protected]", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"prelude-ls": ["[email protected]", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["[email protected]", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"prettier-plugin-svelte": ["[email protected]", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^5.0.0" } }, "sha512-oDVmtKi+M8bJeUoMfPvulUqZYcuXrs5AmhhLYPKtBeg6hcpMdx7UYYisVCqEaLQuKtiPSYFpotfwp4cZK3D4xw=="],
"process-nextick-args": ["[email protected]", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"react": ["[email protected]", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
"readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"rolldown": ["[email protected]", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
"runed": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
"rxjs": ["[email protected]", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"sade": ["[email protected]", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"safe-buffer": ["[email protected]", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"sass": ["[email protected]", "", { "dependencies": { "chokidar": "^5.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ=="],
"sass-embedded": ["[email protected]", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.1.5", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.100.0", "sass-embedded-android-arm": "1.100.0", "sass-embedded-android-arm64": "1.100.0", "sass-embedded-android-riscv64": "1.100.0", "sass-embedded-android-x64": "1.100.0", "sass-embedded-darwin-arm64": "1.100.0", "sass-embedded-darwin-x64": "1.100.0", "sass-embedded-linux-arm": "1.100.0", "sass-embedded-linux-arm64": "1.100.0", "sass-embedded-linux-musl-arm": "1.100.0", "sass-embedded-linux-musl-arm64": "1.100.0", "sass-embedded-linux-musl-riscv64": "1.100.0", "sass-embedded-linux-musl-x64": "1.100.0", "sass-embedded-linux-riscv64": "1.100.0", "sass-embedded-linux-x64": "1.100.0", "sass-embedded-unknown-all": "1.100.0", "sass-embedded-win32-arm64": "1.100.0", "sass-embedded-win32-x64": "1.100.0" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw=="],
"sass-embedded-all-unknown": ["[email protected]", "", { "dependencies": { "sass": "1.100.0" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-auFtXY/kwYILmSVjtBDwyj0axcLbYYiffOKWoaXHnI5bsYwiRbBh3EneR1rpbX2ZIZCrwX93i5pxKLTZF/662Q=="],
"sass-embedded-android-arm": ["[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-70f3HgX2pFNmzpGQ86n5e6QfWn2fP4QUQGfFQK0P1XH73ZLIzLo2YqygrGKGKeeqtc5eU2Wl1/xQzhzuKnO4kw=="],
"sass-embedded-android-arm64": ["[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-W+Ru9JwTnfU0UX3jSZcbqFdtKFMcYdfFwytc57h2DgnqCOIiAqI2E06mABZBZC+r3LwXCBuS5GbXAGeVgvVDkA=="],
"sass-embedded-android-riscv64": ["[email protected]", "", { "os": "android", "cpu": "none" }, "sha512-icU3o0V/uCSytSpf+tX5Lf51BvyQEbLzDUJfUi9etSauYBGHpPKkdtdZH0si4v98phq11Kl8rSV1SggksxF1Hg=="],
"sass-embedded-android-x64": ["[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-mevF9VQk6gEYByy8+jusaHGmd7Usb2ytX/DsEOd0JtOGCtcf1kh575xJ6OUBDIcJ15uLnbau/0iy1eP6WVBvWA=="],
"sass-embedded-darwin-arm64": ["[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1PVlYi61POo93IT/FfrG1mc1tAHxeSTyUALF2aOFmXGWjVXr3bQzEQiBGCOvQbj/ix+5hNyXFXcEMEyKvtUJJA=="],
"sass-embedded-darwin-x64": ["[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-x97o3JnGyImZNCIVs9wQHJUE5QCvmVIKaH1cwrz/5dK7OT1FpeNiW+u9TUomP9hG6Ekjd8EL8NBHpxTfIhdjmg=="],
"sass-embedded-linux-arm": ["[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw=="],
"sass-embedded-linux-arm64": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Dwjmj8Z6VRy7rAi53JAdEwIyUjpfl7PhpSc2/LpQPQx+aO5Dp7Spaipkax0ufJl1SoDUdchCsM4y/88YaluorQ=="],
"sass-embedded-linux-musl-arm": ["[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw=="],
"sass-embedded-linux-musl-arm64": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg=="],
"sass-embedded-linux-musl-riscv64": ["[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ=="],
"sass-embedded-linux-musl-x64": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA=="],
"sass-embedded-linux-riscv64": ["[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ=="],
"sass-embedded-linux-x64": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA=="],
"sass-embedded-unknown-all": ["[email protected]", "", { "dependencies": { "sass": "1.100.0" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A=="],
"sass-embedded-win32-arm64": ["[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-iE+yxj+hUXwwbqpHkXxgAWTzeRfcWxJ7SSTQEPMk48lwq3oCrWLlz5sQuWHbuTK/i0GKQfROdP+hOmPi89yjUg=="],
"sass-embedded-win32-x64": ["[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-qI4F8MI7/KYoy9NdjJfhSspG42WPkADSNDvwEV7qWvCSFC83koJssRsKO2/PfY+niZz6BG65Ic/D+A11h959hw=="],
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"set-cookie-parser": ["[email protected]", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
"setimmediate": ["[email protected]", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"shebang-command": ["[email protected]", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["[email protected]", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"sileo": ["[email protected]", "", { "dependencies": { "motion": "^12.34.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-Fc/3VNFWgQlZttkyvAdnPlzWrLTaii6FIas7nMivlx3uGuKpuNNRZGtSgNRFqTHFn70ZxEjKYxsBIO0nQlDGAg=="],
"sirv": ["[email protected]", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string_decoder": ["[email protected]", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"style-to-object": ["[email protected]", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="],
"supports-color": ["[email protected]", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"svelte": ["[email protected]", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.9", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-fTjjT8cHLDwigcu2j3pv7Jq04LklXevPB8uBgyHNiTXv+RMNvVnrjS4UEYrLMkhuq1vpCodHjiW+z/95SDs/fg=="],
"svelte-check": ["[email protected]", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w=="],
"svelte-confetti": ["[email protected]", "", { "peerDependencies": { "svelte": ">=5.0.0" } }, "sha512-cfIwoGqMPYWRYDUz2g7mG1uHYWy7VBepelQdzCC3j/M42UrAqaBYmIi9xaoQfow4fbINHO9WuARnTyK2bjjGQg=="],
"svelte-eslint-parser": ["[email protected]", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0", "semver": "^7.7.2" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-hhvSH6kRj46UzrBVO5TaotD+Iuvruj5ccKBcO4wAhVcPTLmIc/c32D8UllBTYO0on4LzYuM0rNzf1lM/gBlkSQ=="],
"svelte-toolbelt": ["[email protected]", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
"sync-child-process": ["[email protected]", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="],
"sync-message-port": ["[email protected]", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="],
"tabbable": ["[email protected]", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
"tailwind-merge": ["[email protected]", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
"tailwind-variants": ["[email protected]", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
"tailwindcss": ["[email protected]", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
"tapable": ["[email protected]", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"to-regex-range": ["[email protected]", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"torph": ["[email protected]", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18", "svelte": ">=5", "vue": ">=3" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-DOirbURFAedEZlK6Mx9jcAc7/gbDBUZNyXQwHUmQRXyUsbB3STYwb2PjiAy9iMc1eOWoQLfhm54DuPhWBPEibg=="],
"totalist": ["[email protected]", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"ts-api-utils": ["[email protected]", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["[email protected]", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-check": ["[email protected]", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"typescript-eslint": ["[email protected]", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.0", "@typescript-eslint/parser": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw=="],
"undici-types": ["[email protected]", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"uri-js": ["[email protected]", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util-deprecate": ["[email protected]", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["[email protected]", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"varint": ["[email protected]", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
"vite": ["[email protected]", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
"vite-plugin-devtools-json": ["[email protected]", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw=="],
"vitefu": ["[email protected]", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["[email protected]", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yaml": ["[email protected]", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"yocto-queue": ["[email protected]", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zimmerframe": ["[email protected]", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["[email protected]", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/config-array/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/[email protected]", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@jridgewell/remapping/@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@parcel/watcher/detect-libc": ["[email protected]", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/[email protected]", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tauri-apps/plugin-shell/@tauri-apps/api": ["@tauri-apps/[email protected]", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
"@typescript-eslint/eslint-plugin/ignore": ["[email protected]", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/parser/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/project-service/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/type-utils/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/typescript-estree/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/typescript-estree/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/[email protected]", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"eslint/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"eslint-plugin-svelte/globals": ["[email protected]", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
"micromatch/picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"sass/chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"svelte-eslint-parser/eslint-scope": ["[email protected]", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"svelte-eslint-parser/eslint-visitor-keys": ["[email protected]", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"svelte-eslint-parser/espree": ["[email protected]", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"@eslint/config-array/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"@jridgewell/remapping/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/remapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["[email protected]", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"eslint/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"sass/chokidar/readdirp": ["[email protected]", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"svelte-eslint-parser/espree/acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"@eslint/config-array/minimatch/brace-expansion/balanced-match": ["[email protected]", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["[email protected]", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"eslint/minimatch/brace-expansion/balanced-match": ["[email protected]", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "gray"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
+71
View File
@@ -0,0 +1,71 @@
const path = require('path');
const fs = require('fs');
const configFolder = path.join(process.platform == "win32" ? process.env['LOCALAPPDATA'] : process.env['HOME'], 'EZPPLauncher');
if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder);
const configLocation = path.join(configFolder, `ezpplauncher.${path.basename(process.env['USERNAME'])}.cfg`);
if (!fs.existsSync(configLocation)) fs.writeFileSync(configLocation, "");
async function get(key, defaultValue) {
const fileStream = await fs.promises.readFile(configLocation, "utf-8");
const lines = fileStream.split(/\r?\n/)
for (const line of lines) {
if (line.includes('=')) {
const argsPair = line.split('=', 2);
const keyname = argsPair[0]
const value = argsPair[1];
if (keyname == key) {
return value;
}
}
}
return defaultValue;
}
async function set(key, value) {
const configValues = new Map();
const fileStream = await fs.promises.readFile(configLocation, "utf-8");
const lines = fileStream.split(/\r?\n/)
for (const line of lines) {
if (line.includes('=')) {
const argsPair = line.split('=', 2);
const keyname = argsPair[0]
const value = argsPair[1];
configValues.set(keyname, value);
}
}
configValues.set(key, value);
const arr = [];
for (var [storkey, storvalue] of configValues.entries())
arr.push(`${storkey}=${storvalue}`);
await fs.promises.writeFile(configLocation, arr.join('\n'));
}
async function remove(key) {
const configValues = new Map();
const fileStream = await fs.promises.readFile(configLocation, "utf-8");
const lines = fileStream.split(/\r?\n/)
for (const line of lines) {
if (line.includes('=')) {
const argsPair = line.split('=', 2);
const keyname = argsPair[0]
const value = argsPair[1];
configValues.set(keyname, value);
}
}
const arr = [];
for (var [storkey, storvalue] of configValues.entries()) {
if (storkey != key)
arr.push(`${storkey}=${storvalue}`);
}
await fs.promises.writeFile(configLocation, arr.join('\n'));
}
module.exports = { get, set, remove }
+61
View File
@@ -0,0 +1,61 @@
const appInfo = require('./appInfo.js');
const DiscordAutoRPC = require("discord-auto-rpc");
const { app } = require('electron');
const DiscordRPC = require("discord-rpc").default;
const clientId = "1032772293220384808";
let client = undefined;
let lastState = "Idle in Launcher...";
let presenceEnabled = true;
let startDate = new Date();
const actionButtons = [
{
label: "Download the Launcher",
url: "https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest"
},
{
label: "Join EZPPFarm",
url: "https://ez-pp.farm/discord"
}
]
let lastActivity = {
details: " ",
state: lastState,
startTimestamp: startDate,
largeImageKey: "ezppfarm",
largeImageText: appInfo.appName + " " + appInfo.appVersion,
buttons: actionButtons,
instance: false,
};
module.exports = {
connect: () => {
if (client === undefined) {
client = new DiscordAutoRPC.AutoClient({ transport: "ipc" });
client.endlessLogin({ clientId: clientId });
client.once("ready", () => {
setInterval(() => {
if (lastActivity !== undefined)
lastActivity.state = lastState;
client.setActivity(presenceEnabled ? lastActivity : undefined);
}, 2500);
});
}
},
enablePresence: () => presenceEnabled = true,
disablePresence: () => presenceEnabled = false,
updateStartDate: () => startDate = new Date(),
updateState: (state) => lastState = state,
updateStatus: (details, osuVersion) => {
lastActivity = {
details: details ? details : " ",
state: lastState,
startTimestamp: startDate,
smallImageKey: osuVersion ? "osu" : " ",
smallImageText: osuVersion ? osuVersion : " ",
largeImageKey: "ezppfarm",
largeImageText: appInfo.appName + " " + appInfo.appVersion,
buttons: actionButtons,
instance: false,
}
}
}
-53
View File
@@ -1,53 +0,0 @@
import svelteConfig from './svelte.config.js';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
{
// Tauri build output is ignored by src-tauri/.gitignore, not the root file.
ignores: ['src-tauri/target/**', 'src-tauri/gen/**'],
},
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node },
},
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off',
},
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig,
},
},
},
{
rules: {
'no-empty': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^\\$\\$(Props|Events|Slots|Generic)$' },
],
},
}
);
+18
View File
@@ -0,0 +1,18 @@
const childProcess = require('child_process');
module.exports = {
runFile: (folder, file, args, onExit) => {
childProcess.execFile(file, args, {
cwd: folder
}, (_err, _stdout, _stderr) => {
onExit();
});
},
runFileDetached: (folder, file, args) => {
const subprocess = childProcess.spawn(file + " " + args, {
cwd: folder,
detached: true,
stdio: 'ignore'
})
subprocess.unref()
}
}
+10
View File
@@ -0,0 +1,10 @@
const axios = require('axios').default;
const loginCheckEndpoint = 'https://ez-pp.farm/login/check';
const performLogin = async (username, password) => {
const result = await axios.post(loginCheckEndpoint, { username, password });
return result.data;
}
module.exports = { performLogin };
+15
View File
@@ -0,0 +1,15 @@
const fs = require('fs');
async function existsAsync(filePath) {
return new Promise(function (resolve, _reject) {
fs.stat(filePath, function (err, _stat) {
if (err == null) {
resolve(true)
} else {
resolve(false);
}
})
})
}
module.exports = { existsAsync };
+121
View File
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>EZPPLauncher</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="../assets/logo.png" />
<link href="../assets/mdb.min.css" rel="stylesheet" />
<link href="../assets/launcher.css" rel="stylesheet" />
<link href="../assets/checkbox.css" rel="stylesheet" />
<style>
* {
user-select: none;
-webkit-user-select: none;
}
</style>
</head>
<body class="fixed-sn mdb-skin-custom" data-spy="scroll" data-target="#scrollspy" data-offset="15"
oncontextmenu="return false;">
<main>
<div class="noselect">
<div class="position-relative overflow-hidden p-1 w-100 text-center text-lg-end d-flex align-items-center justify-content-center"
style="border-radius: 0.5em;">
<div class="launcher-window position-relative overflow-hidden">
<div class="container px-1 py-2 w-100 mw-100 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div id="loading-page" class="sections col col-xl-10">
<div class="launch-section flex-row">
<div class="server-logo">
<img src="../assets/logo.png" height="120">
</div>
<div class="app-name">EZPPLauncher</div>
</div>
<div class="loading-section">
<div class="loading-indicator">
<div class="loader">
<svg class="circular-loader" viewBox="25 25 50 50">
<circle class="loader-path" cx="50" cy="50" r="20" fill="none"
stroke="#fff" stroke-width="2" />
</svg>
</div>
</div>
<div class="loading-indicator-text">Loading... Please wait</div>
</div>
</div>
<div id="launch-page" class="sections col col-xl-10" style="display: none;">
<div class="account-section">
<div class="user-image">
</div>
<div class="user-info">
<div class="welcome-text" id="welcome-text">
Nice to see you!
</div>
<div class="account-action" id="account-action">
Click to login
</div>
</div>
</div>
<div class="launch-section">
<div class="server-logo">
<img src="../assets/logo.png" height="150">
</div>
<div class="launch-button-section">
<button class="btn btn-lg btn-launch btn-accent" id="launch-btn">Launch</button>
<div class="patch-checkbox" style="display: none;">
<input type="checkbox" id="enablePatching" disabled />
<label for="enablePatching" style="display: initial;">enable
Patching</label>
</div>
</div>
</div>
<div class="folder-section">
<div class="folder-location">
Current osu! directory: <span id="currentOsuPath"></span>
</div>
<div class="folder-action" id="change-folder-btn">
Not correct?
</div>
</div>
</div>
<div id="login-page" class="sections col col-xl-10" style="display: none;">
<div class="launch-section flex-row">
<div class="server-logo">
<img src="../assets/logo.png" height="120">
</div>
<div class="app-name">EZPPLauncher</div>
</div>
<div class="login-section">
<div class="form-outline mb-3 w-50">
<input type="text" id="login-username" class="form-control form-control-lg" />
<label class="form-label" for="login-username">Username</label>
</div>
<div class="form-outline mb-3 w-50">
<input type="password" id="login-password"
class="form-control form-control-lg" />
<label class="form-label" for="login-password">Password</label>
</div>
<div class="pt-1 mb-4">
<div class="btn-grouped">
<button id="action-cancel" class="btn btn-grey btn-lg"
type="button">Cancel</button>
<button id="action-login" class="btn btn-accent btn-lg"
type="button">Login</button>
</div>
</div>
<p class="text-muted clickable" id="register">Don't have an account?</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
<script type="text/javascript" src="../assets/mdb.min.js"></script>
</html>
+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>EZPPLauncher</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="../assets/logo.png" />
<link href="../assets/mdb.min.css" rel="stylesheet" />
<style>
* {
user-select: none;
-webkit-user-select: none;
}
</style>
</head>
<body class="fixed-sn mdb-skin-custom" data-spy="scroll" data-target="#scrollspy" data-offset="15"
oncontextmenu="return false;">
<main>
<div class="noselect">
<div class="position-relative overflow-hidden p-3 p-md-5 m-md-3 text-center text-lg-end d-flex align-items-center justify-content-center"
style="border-radius: 0.5em;">
<div class="position-relative overflow-hidden p-3 p-md-5 m-md-3">
<div class="container py-2 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col col-xl-10">
<div class="card" style="border-radius: 1rem;">
<div class="row g-0">
<div class="card-body p-4 p-lg-5 text-black">
<div class="d-flex align-items-center mb-2 pb-1 text-white">
<span class="h1 fw-bold mb-0">EZPPLauncher</span>
</div>
<h5 class="fw-lighter fs-5 mb-3 pb-3 text-white text-start"
style="letter-spacing: 1px;">
Launch osu! with connection to the EZPPFarm server
</h5>
<div class="pt-1 mb-4">
<button id="launch-btn" class="btn btn-primary btn-lg btn-block"
type="button" style="background-color:#d6016f" disabled>Looking for
updates...</button>
</div>
<button class="btn btn-dark btn-sm float-start" id="account-btn" disabled>
set account
</button>
<button class="btn btn-dark btn-sm float-end" id="folder-btn">
set osu! directory
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
<script type="text/javascript" src="../assets/mdb.min.js"></script>
</html>
+17
View File
@@ -0,0 +1,17 @@
const fs = require("fs");
const getLinuxDistroInfo = async() => {
let os = await fs.promises.readFile('/etc/os-release', 'utf8')
let opj = {}
os?.split('\n')?.forEach((line, index) => {
let words = line?.split('=')
let key = words[0]?.toLowerCase()
if (key === '') return
let value = words[1]?.replace(/"/g, '')
opj[key] = value
})
return opj;
}
module.exports = { getLinuxDistroInfo };
+196
View File
@@ -0,0 +1,196 @@
const fs = require('fs');
const fu = require('./fileUtil');
const path = require('path');
const crypto = require('crypto');
const axios = require('axios').default;
const executeUtil = require('./executeUtil');
const { EventEmitter } = require('events');
const { DownloaderHelper } = require('node-downloader-helper');
const checkUpdateURL = "https://osu.ppy.sh/web/check-updates.php?action=check&stream=";
const ignoredOsuEntities = [
'osu!auth.dll'
]
const osuEntities = [
'avcodec-51.dll',
'avformat-52.dll',
'avutil-49.dll',
'bass.dll',
'bass_fx.dll',
'collection.db',
'd3dcompiler_47.dll',
'Data',
'Downloads',
'libEGL.dll',
'libGLESv2.dll',
'Logs',
'Microsoft.Ink.dll',
'OpenTK.dll',
'osu!.cfg',
'osu!.db',
'osu!.exe',
'osu!auth.dll',
'osu!gameplay.dll',
'osu!seasonal.dll',
'osu!ui.dll',
'presence.db',
'pthreadGC2.dll',
'scores.db',
'Skins',
'Songs'
]
async function isValidOsuFolder(path) {
const allFiles = await fs.promises.readdir(path);
let matches = 0;
for (const file of allFiles) {
if (osuEntities.includes(file)) matches = matches + 1;
}
return Math.round((matches / osuEntities.length) * 100) >= 60;
}
async function getLatestConfig(osuPath) {
const configFileInfo = {
name: "",
path: "",
get: async (key) => {
if (!configFileInfo.path)
return "";
const fileStream = await fs.promises.readFile(configFileInfo.path, "utf-8");
const lines = fileStream.split(/\r?\n/)
for (const line of lines) {
if (line.includes(' = ')) {
const argsPair = line.split(' = ', 2);
const keyname = argsPair[0]
const value = argsPair[1];
if (keyname == key) {
return value;
}
}
}
}
}
const userOsuConfig = path.join(osuPath, `osu!.${process.env['USERNAME']}.cfg`)
if (fs.existsSync(userOsuConfig)) {
configFileInfo.name = `osu!.${process.env['USERNAME']}.cfg`;
configFileInfo.path = userOsuConfig;
}
return configFileInfo;
}
async function getUpdateFiles(releaseStream) {
const releaseData = await axios.get(checkUpdateURL + releaseStream, {});
return releaseData.data;
}
async function filesThatNeedUpdate(osuPath, updateFiles) {
const filesToDownload = [];
for (const updatedFile of updateFiles) {
const fileName = updatedFile.filename;
const fileHash = updatedFile.file_hash;
const fileURL = updatedFile.url_full;
if (ignoredOsuEntities.includes(fileName)) continue;
const fileOnDisk = path.join(osuPath, fileName);
if (await fu.existsAsync(fileOnDisk)) {
const binaryFileContents = await fs.promises.readFile(fileOnDisk);
const existingFileMD5 = crypto.createHash("md5").update(binaryFileContents).digest("hex");
if (existingFileMD5.toLowerCase() != fileHash.toLowerCase()) {
filesToDownload.push({
fileName,
fileURL
})
}
} else {
filesToDownload.push({
fileName,
fileURL
});
}
}
return filesToDownload;
}
async function downloadUpdateFiles(osuPath, filesToUpdate) {
const eventEmitter = new EventEmitter();
let completedIndex = 0;
filesToUpdate.forEach(async (fileToUpdate) => {
const filePath = path.join(osuPath, fileToUpdate.fileName);
console.log(filePath);
if (await fu.existsAsync(filePath))
await fs.promises.rm(filePath);
const fileDownload = new DownloaderHelper(fileToUpdate.fileURL, osuPath, {
fileName: fileToUpdate.fileName,
override: true,
});
fileDownload.on('end', () => {
completedIndex = completedIndex + 1;
if (completedIndex >= filesToUpdate.length)
eventEmitter.emit('completed');
});
fileDownload.on('error', (err) => {
console.log(err);
eventEmitter.emit('error');
});
fileDownload.start().catch(err => console.error(err));
});
return eventEmitter;
}
async function startWithDevServer(osuPath, serverDomain, onExit) {
const osuExe = path.join(osuPath, "osu!.exe");
if (!await fu.existsAsync(osuExe)) return false;
switch (process.platform) {
case "linux":
executeUtil.runFile(osuPath, 'osu-stable', ["-devserver", serverDomain], onExit);
return true;
case "win32":
executeUtil.runFile(osuPath, osuExe, ["-devserver", serverDomain], onExit);
return true;
}
return false;
}
async function setConfigValue(configPath, key, value) {
const configLines = new Array();
const fileStream = await fs.promises.readFile(configPath, "utf-8");
const lines = fileStream.split(/\r?\n/)
for (const line of lines) {
if (line.includes(' = ')) {
const argsPair = line.split(' = ', 2);
const keyname = argsPair[0].trim();
if (key == keyname) {
configLines.push(`${keyname} = ${value}`);
} else {
configLines.push(line);
}
} else {
configLines.push(line);
}
}
await fs.promises.writeFile(configPath, configLines.join("\n"), 'utf-8');
}
async function findOsuInstallation() {
const regedit = require('qiao-regedit');
const osuLocationFromDefaultIcon = "HKLM\\SOFTWARE\\Classes\\osu\\DefaultIcon";
const osuStruct = await regedit.listValuesSync(osuLocationFromDefaultIcon);
for (const line of osuStruct.split("\n")) {
if (line.includes("REG_SZ")) {
let value = (line.trim().split(" ")[2]);
value = value.substring(1, value.length - 3);
return value.trim();
}
}
return undefined;
}
module.exports = {
isValidOsuFolder, getLatestConfig, getUpdateFiles, filesThatNeedUpdate,
downloadUpdateFiles, startOsuWithDevServer: startWithDevServer, setConfigValue,
findOsuInstallation
}
+51 -74
View File
@@ -1,81 +1,58 @@
{
"name": "ezpplauncher",
"version": "3.2.7",
"description": "",
"type": "module",
"scripts": {
"debug": "bun run tauri dev",
"build": "bun ./scripts/build.ts",
"vite:dev": "vite dev",
"vite:build": "vite build && exit 0",
"tauri:build": "tauri build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri",
"tauri:update-logo": "bun run tauri icon ./static/logo.png",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
"version": "1.1.2",
"main": "app.js",
"license": "MIT",
"author": "HorizonCode",
"build": {
"appId": "farm.ezpp.ezppfarm.launcher",
"productName": "ezpplauncher",
"directories": {
"output": "release",
"buildResources": "dist"
},
"license": "GPL-3.0-only",
"dependencies": {
"@better-fetch/fetch": "^1.1.21",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@fontsource-variable/montserrat": "^5.3.0",
"@fontsource-variable/playfair-display": "^5.3.0",
"@fontsource-variable/sora": "^5.2.8",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/space-mono": "^5.2.9",
"@number-flow/svelte": "^0.4.0",
"@tailwindcss/vite": "^4.3.0",
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@types/node": "^25.9.1",
"animejs": "^4.4.1",
"buffer": "^6.0.3",
"crypto-js": "^4.2.0",
"jszip": "^3.10.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"semver": "^7.8.1",
"sileo": "^0.1.5",
"svelte-confetti": "^2.3.2",
"torph": "^0.1.3",
"tw-animate-css": "^1.4.0"
"asar": true,
"win": {
"icon": "./assets/logo.png",
"target": [
"portable"
]
},
"nsis": {
"runAfterFinish": true
},
"portable": {
"artifactName": "EZPPLauncher.exe"
}
},
"frontend": {
"config": {
"applicationName": "EZPPLauncher"
}
},
"scripts": {
"start": "electron .",
"rebuild": "electron-rebuild -f -w get-window-by-name",
"pack-win": "electron-builder --win --arm64 --x64",
"pack-linux": "electron-builder --linux --arm64 --x64",
"pack-mac": "electron-builder --mac --arm64 --x64",
"dist": "electron-builder"
},
"devDependencies": {
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@iarna/toml": "2.2.5",
"@lucide/svelte": "^1.16.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.61.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tauri-apps/cli": "^2.11.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/bun": "^1.3.14",
"@types/crypto-js": "^4.2.2",
"@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1",
"bits-ui": "^2.18.1",
"clsx": "^2.1.1",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.17.1",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^4.0.1",
"sass-embedded": "^1.100.0",
"svelte": "^5.55.9",
"svelte-check": "^4.4.8",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.3.0",
"tslib": "^2.8.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.0",
"vite": "^8.0.14",
"vite-plugin-devtools-json": "^1.0.0"
"electron": "^24.2.0",
"electron-builder": "^23.6.0",
"electron-packager": "^17.1.1",
"electron-rebuild": "^3.2.9"
},
"dependencies": {
"axios": "^0.27.2",
"custom-electron-titlebar": "^4.1.1",
"discord-auto-rpc": "^1.0.17",
"discord-rpc": "^4.0.1",
"get-window-by-name": "^2.0.0",
"jquery": "^3.6.0",
"node-downloader-helper": "^2.1.4",
"qiao-regedit": "^0.1.5",
"sweetalert2": "^11.5.2"
}
}
+233
View File
@@ -0,0 +1,233 @@
const { ipcRenderer } = require('electron');
const { Titlebar, Color } = require('custom-electron-titlebar');
const appInfo = require('../appInfo');
let titlebar;
let currentPage = "loading";
let loggedIn = false;
window.addEventListener('DOMContentLoaded', () => {
titlebar = new Titlebar({
backgroundColor: Color.fromHex("#24283B"),
itemBackgroundColor: Color.fromHex("#121212"),
menu: null,
maximizable: false
});
titlebar.updateTitle(`${appInfo.appName} ${appInfo.appVersion}`);
const $ = require('jquery');
const Swal = require('sweetalert2');
$('#account-action').on('click', () => {
if (!loggedIn) {
changePage('login');
} else {
$('#welcome-text').text(`Nice to see you!`);
$('#account-action').text('Click to login');
$('.user-image').css('background-image', `url(https://a.ez-pp.farm/0)`)
loggedIn = false;
ipcRenderer.send("perform-logout");
Swal.fire({
title: 'See ya soon!',
text: "Successfully logged out!",
icon: 'success',
confirmButtonText: 'Okay'
});
}
});
$('#action-cancel').on('click', () => {
if (!loggedIn) {
changePage('launch');
}
});
let currentState;
$("#launch-btn").on('click', async () => {
switch (currentState) {
case "up-to-date":
$("#launch-btn").attr('disabled', true);
$('#launch-btn').html('Launching...');
const result = await ipcRenderer.invoke("launch");
if (!result) {
Swal.fire({
title: 'Uh oh!',
text: "Something went wrong while launching!",
icon: 'error',
confirmButtonText: 'Okay'
});
$("#launch-btn").attr('disabled', false);
$('#launch-btn').html('Launch');
} else {
$("#launch-btn").attr('disabled', true);
$('#launch-btn').html('Running...');
}
break;
case "update-available":
$("#launch-btn").attr('disabled', true);
$('#launch-btn').html('Updating...');
ipcRenderer.send("do-update");
break;
case "missing-folder":
const responseData = await ipcRenderer.invoke('set-osu-dir');
if (!responseData)
return;
if (responseData.validOsuDir) {
Swal.fire({
title: 'Success!',
text: 'osu! folder set.',
icon: 'success',
confirmButtonText: 'Cool'
})
$('#currentOsuPath').text(responseData.folderPath);
ipcRenderer.send("do-update-check");
} else {
Swal.fire({
title: 'Uh oh!',
text: 'The selected folder is not a osu! directory.',
icon: 'error',
confirmButtonText: 'Oops.. my bad!'
})
}
break;
}
});
$("#action-login").on('click', async () => {
const username = $('#login-username').val();
const password = $('#login-password').val();
$("#action-login").attr('disabled', true);
$("#action-cancel").attr('disabled', true);
const responseData = await ipcRenderer.invoke('perform-login', { username, password });
$("#action-login").attr('disabled', false);
$("#action-cancel").attr('disabled', false);
if (!responseData)
return;
if (responseData.code != 200) {
Swal.fire({
title: 'Uh oh!',
text: responseData.message,
icon: 'error',
confirmButtonText: 'Oops.. my bad!'
})
return;
}
$('#login-username').val("");
$('#login-password').val("");
$('#welcome-text').text(`Welcome back, ${responseData.user.name}!`);
$('#account-action').text('Not you?');
$('.user-image').css('background-image', `url(https://a.ez-pp.farm/${responseData.user.id})`);
loggedIn = true;
changePage('launch');
})
$("#change-folder-btn").on('click', async () => {
const responseData = await ipcRenderer.invoke('set-osu-dir');
if (!responseData)
return;
if (responseData.validOsuDir) {
Swal.fire({
title: 'Success!',
text: 'osu! folder set.',
icon: 'success',
confirmButtonText: 'Cool'
})
$('#currentOsuPath').text(responseData.folderPath);
ipcRenderer.send("do-update-check");
} else {
Swal.fire({
title: 'Uh oh!',
text: 'The selected folder is not a osu! directory.',
icon: 'error',
confirmButtonText: 'Oops.. my bad!'
})
}
});
ipcRenderer.on('config_update', (event, data) => {
if (data.osuPath) {
$('#currentOsuPath').text(data.osuPath);
}
})
ipcRenderer.on('account_update', (event, data) => {
switch (data.type) {
case "not-loggedin":
changePage("launch");
break;
case "loggedin":
changePage("launch");
$('#welcome-text').text(`Welcome back, ${data.user.name}!`);
$('#account-action').text('Not you?');
$('.user-image').css('background-image', `url(https://a.ez-pp.farm/${data.user.id})`);
loggedIn = true;
break;
}
})
ipcRenderer.on('status_update', (event, status) => {
switch (status.type) {
case "up-to-date":
$("#launch-btn").attr('disabled', false);
$('#launch-btn').html('Launch');
currentState = status.type;
break;
case "update-available":
$("#launch-btn").attr('disabled', false);
$('#launch-btn').html('Update');
currentState = status.type;
break;
case "missing-folder":
$("#launch-btn").attr('disabled', false);
$('#launch-btn').html('set your osu! folder');
currentState = status.type;
break;
case "error":
Swal.fire({
title: 'Uh oh!',
text: status.message,
icon: 'error',
confirmButtonText: 'Okay'
});
ipcRenderer.send("do-update-check");
break;
case "info":
Swal.fire({
title: 'Info!',
text: status.message,
icon: 'info',
confirmButtonText: 'Okay'
});
break;
case "package-issue":
Swal.fire({
title: 'Uh oh!',
text: status.message,
icon: 'error',
confirmButtonText: 'Okay'
});
$("#launch-btn").attr('disabled', true);
$('#launch-btn').html('missing packages');
break;
case "update-complete":
Swal.fire({
title: 'Yaaay!',
text: "Your osu! client has been successfully updated!",
icon: 'success',
confirmButtonText: 'Thanks :3'
});
ipcRenderer.send("do-update-check");
break;
}
})
function changePage(page) {
$(`#${currentPage}-page`).fadeOut(50, "swing", () => {
$(`#${page}-page`).fadeIn(350);
});
currentPage = page;
}
// workaround for the dark theme
$('head').append($('<link href="../assets/sweetalert2.dark.css" rel="stylesheet" />'));
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

-40
View File
@@ -1,40 +0,0 @@
import { spawn } from 'child_process';
function runVite(): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn('bunx', ['--bun', 'vite', 'build'], { stdio: ['inherit', 'pipe', 'pipe'] });
proc.stdout?.on('data', (data: Buffer) => {
process.stdout.write(data);
if (data.toString().includes('✔ done')) {
setTimeout(() => {
proc.kill();
resolve();
}, 500);
}
});
proc.stderr?.on('data', (data: Buffer) => process.stderr.write(data));
proc.on('close', (code) => {
if (code === 0 || code === null) resolve();
else reject(new Error(`vite build exited with code ${code}`));
});
});
}
function run(cmd: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn(cmd, args, { stdio: 'inherit' });
proc.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`${cmd} exited with code ${code}`));
});
});
}
await run('bun', ['./scripts/sync-version.ts']);
await runVite();
//set NO_STRIP=1 in env for linux to prevent stripping of debug symbols in tauri builds, which causes issues with source maps
if (process.platform === 'linux') process.env.NO_STRIP = '1';
await run('bunx', ['tauri', 'build']);
-28
View File
@@ -1,28 +0,0 @@
import * as toml from '@iarna/toml';
import fs from 'fs';
import path from 'path';
import { cwd } from 'process';
const packageJsonPath = path.join(cwd(), 'package.json');
const cargoTomlPath = path.join(cwd(), 'src-tauri', 'Cargo.toml');
const tauriConfPath = path.join(cwd(), 'src-tauri', 'tauri.conf.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const version = packageJson.version;
if (!version) throw new Error('Could not find version in package.json');
let cargoTomlContent = fs.readFileSync(cargoTomlPath, 'utf8');
const cargo = toml.parse(cargoTomlContent) as { package: { version: string } };
if (cargo.package.version !== version) {
cargoTomlContent = cargoTomlContent.replace(/version = ".*?"/, `version = "${version}"`);
fs.writeFileSync(cargoTomlPath, cargoTomlContent);
console.log(`✅ Synced Cargo.toml version to ${version}`);
}
const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
if (tauriConf.version !== version) {
tauriConf.version = version;
fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2));
console.log(`✅ Synced tauri.conf.json version to ${version}`);
}
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"types": ["@types/bun", "@types/node"]
},
"include": ["*.ts"]
}
-7
View File
@@ -1,7 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
-7094
View File
File diff suppressed because it is too large. Load diff
-81
View File
@@ -1,81 +0,0 @@
[package]
name = "ezpplauncher"
version = "3.2.7"
description = "EZPPLauncher redefined."
authors = ["HorizonCode"]
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.dev]
incremental = true # Compile your binary in smaller steps.
[profile.release]
codegen-units = 1 # Allows LLVM to perform better optimization.
lto = true # Enables link-time-optimizations.
opt-level = "s" # Prioritizes small binary size. Use `3` if you prefer speed.
panic = "abort" # Higher performance by disabling panic handlers.
strip = true # Ensures debug symbols are removed.
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "ezpplauncher_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
tauri = { version = "2.11.2", features = ["protocol-asset"] }
tauri-plugin-shell = "2.3.5"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
serde_repr = "0.1.20"
tauri-plugin-dialog = "2.7.1"
tauri-plugin-fs = "2.5.1"
hardware-id = "0.3.0"
tauri-plugin-cors-fetch = "5.0.0"
sysinfo = "0.39.2"
reqwest = { version = "0.13.4", features = ["json", "stream", "query"] }
md5 = "0.8.0"
tokio = { version = "1.52.3", features = ["full"] }
open = "5.3.5"
presenceforge = { version = "0.2.1", features = ["tokio-runtime"] }
once_cell = "1.21.4"
futures-util = "0.3.32"
byteorder = "1.5.0"
anyhow = "1.0.102"
url = "2.5.8"
zip = "8.6.0"
sha1 = "0.11.0"
hex = "0.4.3"
rusqlite = { version = "0.40.0", features = ["bundled"] }
aes-gcm = "0.10.3"
base64 = "0.22.1"
sha2 = "0.11.0"
thiserror = "2.0.18"
tauri-plugin-log = "2"
log = "0.4"
time = { version = "0.3", features = ["local-offset"] }
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
http-body-util = "0.1.3"
http-range = "0.1.5"
hyper = { version = "1.8.1", features = ["server", "http1"] }
hyper-util = { version = "0.1.19", features = ["tokio"] }
percent-encoding = "2.3.2"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3"
[target.'cfg(windows)'.dependencies]
winreg = "0.56.0"
winapi = { version = "0.3.9", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] }
widestring = "1.2.1"
windows-sys = "0.61.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.4.2"
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
-150
View File
@@ -1,150 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capabilities for the main launcher and theme editor windows",
"windows": [
"main",
"theme-editor"
],
"permissions": [
"core:default",
"shell:allow-open",
"shell:default",
"shell:allow-execute",
"dialog:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-close",
"core:window:allow-destroy",
"cors-fetch:default",
"core:window:allow-hide",
"core:window:allow-show",
"fs:default",
{
"identifier": "fs:allow-read",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-write",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-exists",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-read-file",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-write-file",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-mkdir",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-read-dir",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-remove",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

-21
View File
@@ -1,21 +0,0 @@
!macro NSIS_HOOK_POSTINSTALL
${If} $PassiveMode = 1
${OrIf} ${Silent}
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
${EndIf}
!macroend
!macro NSIS_HOOK_PREUNINSTALL
${If} $DeleteAppDataCheckboxState = 1
MessageBox MB_YESNO "Do you want to remove all EZPPLauncher data and themes?" IDYES do_delete IDNO skip_delete
do_delete:
StrCpy $R9 1
skip_delete:
${EndIf}
!macroend
!macro NSIS_HOOK_POSTUNINSTALL
${If} $R9 = 1
RMDir /r "$PROFILE\.ezpplauncher"
${EndIf}
!macroend
File diff suppressed because it is too large. Load diff
-219
View File
@@ -1,219 +0,0 @@
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit, OsRng},
};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rusqlite::{Connection, OptionalExtension, params};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("SQLite error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("Encryption error: {0}")]
Encryption(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Base64 error: {0}")]
Base64(#[from] base64::DecodeError),
#[error("Config not initialized")]
NotInitialized,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
impl serde::Serialize for ConfigError {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
pub struct Config {
db_path: PathBuf,
conn: Option<Connection>,
hwid_key: Option<[u8; 32]>,
}
impl Config {
pub fn new(db_path: impl Into<PathBuf>) -> Self {
Self {
db_path: db_path.into(),
conn: None,
hwid_key: None,
}
}
pub fn init(&mut self, hwid: &str) -> Result<bool, ConfigError> {
let mut hasher = Sha256::new();
hasher.update(hwid.as_bytes());
let key_bytes: [u8; 32] = hasher.finalize().into();
self.hwid_key = Some(key_bytes);
let is_new = !self.db_path.exists();
if let Some(parent) = self.db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(&self.db_path)?;
conn.execute_batch(
"
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
encrypted INTEGER NOT NULL DEFAULT 0
);
",
)?;
self.conn = Some(conn);
Ok(is_new)
}
pub fn get(&self, key: &str) -> Result<Option<Value>, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let mut stmt = conn.prepare("SELECT value, encrypted FROM config WHERE key = ?1")?;
let row: Option<(String, bool)> = stmt
.query_row(params![key], |r| Ok((r.get(0)?, r.get(1)?)))
.optional()?;
match row {
None => Ok(None),
Some((raw, false)) => Ok(Some(serde_json::from_str::<Value>(&raw)?)),
Some((raw, true)) => {
let plaintext = self
.decrypt_value(&raw)
.map_err(|_| ConfigError::Encryption(format!("failed to decrypt '{}'", key)))?;
serde_json::from_str::<Value>(&plaintext)
.map(Some)
.map_err(|_| {
ConfigError::Encryption(format!("decrypted '{}' is not valid JSON", key))
})
}
}
}
pub fn exists(&self, key: &str) -> Result<bool, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM config WHERE key = ?1",
params![key],
|r| r.get(0),
)?;
Ok(count > 0)
}
/// Return all key/value pairs (decrypting encrypted ones).
pub fn all(&self) -> Result<Vec<(String, Value)>, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let mut stmt = conn.prepare("SELECT key, value, encrypted FROM config")?;
let rows = stmt.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, bool>(2)?,
))
})?;
let mut out = Vec::new();
for row in rows {
let (k, raw, enc) = row?;
let v: Value = if enc {
let pt = self.decrypt_value(&raw)?;
serde_json::from_str(&pt)?
} else {
serde_json::from_str(&raw)?
};
out.push((k, v));
}
Ok(out)
}
pub fn set<T: serde::Serialize>(
&self,
key: &str,
value: T,
encrypt: bool,
) -> Result<(), ConfigError> {
let json = serde_json::to_string(&value)?;
if encrypt {
let blob = self.encrypt_value(&json)?;
self.upsert(key, &blob, true)
} else {
self.upsert(key, &json, false)
}
}
pub fn delete(&self, key: &str) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute("DELETE FROM config WHERE key = ?1", params![key])?;
Ok(())
}
pub fn clear(&self) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute("DELETE FROM config", [])?;
Ok(())
}
fn upsert(&self, key: &str, raw: &str, encrypted: bool) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute(
"INSERT INTO config (key, value, encrypted)
VALUES (?1, ?2, ?3)
ON CONFLICT(key) DO UPDATE SET value = excluded.value,
encrypted = excluded.encrypted",
params![key, raw, encrypted as i32],
)?;
Ok(())
}
fn cipher(&self) -> Result<Aes256Gcm, ConfigError> {
let key_bytes = self.hwid_key.ok_or(ConfigError::NotInitialized)?;
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
Ok(Aes256Gcm::new(key))
}
fn encrypt_value(&self, plaintext: &str) -> Result<String, ConfigError> {
let cipher = self.cipher()?;
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext.as_bytes())
.map_err(|e| ConfigError::Encryption(e.to_string()))?;
let mut combined = nonce_bytes.to_vec();
combined.extend_from_slice(&ciphertext);
Ok(BASE64.encode(&combined))
}
fn decrypt_value(&self, blob: &str) -> Result<String, ConfigError> {
let cipher = self.cipher()?;
let combined = BASE64.decode(blob)?;
if combined.len() < 12 {
return Err(ConfigError::Encryption("blob too short".into()));
}
let (nonce_bytes, ciphertext) = combined.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| ConfigError::Encryption(e.to_string()))?;
String::from_utf8(plaintext).map_err(|e| ConfigError::Encryption(e.to_string()))
}
}
-236
View File
@@ -1,236 +0,0 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use hardware_id::get_id;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::Manager;
use tauri_plugin_fs::FsExt;
pub mod commands;
pub mod config;
pub mod logging;
#[cfg(target_os = "macos")]
pub mod macos;
pub mod osudb;
pub mod presence;
pub mod state;
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub mod theme_server;
pub mod utils;
use crate::{
commands::{
backup_theme_folder, check_for_corruption, config_all, config_clear, config_delete,
config_exists, config_get, config_set, download_and_extract_theme,
download_ezpp_launcher_update, download_ezpp_launcher_update_files, encrypt_string, exit,
export_theme, extract_theme, file_size, find_osu_installation, get_beatmapsets_count,
get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream,
get_osu_skin, get_osu_version, get_platform, get_skins_count, has_net8, has_osuwinello,
has_wmctrl, install_ezpp_launcher_update, installation_diagnostics,
is_open_tablet_driver_path_valid, is_open_tablet_driver_running, is_osu_running,
list_theme_backups, log_frontend, open_logs_folder, open_theme_editor, open_theme_folder,
open_url_in_browser, opened_urls, optimize_theme_media, pick_osu_installation,
presence_connect, presence_disconnect, presence_is_connected, presence_update_button,
presence_update_status, presence_update_user, read_theme_info, replace_ui_files,
resolve_osu_folder, restore_theme_backup, run_open_tablet_driver, run_osu, run_osu_updater,
save_theme_project, set_logging_enabled, set_osu_config_values, set_osu_user_config_values,
stop_open_tablet_driver, theme_asset_base, theme_editor_context, theme_preview_url,
valid_osu_folder,
},
state::AppState,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut builder = tauri::Builder::default().plugin(logging::plugin());
#[cfg(desktop)]
{
builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
let app_window = app.get_webview_window("main").expect("no main window");
app_window.set_focus().expect("failed to focus");
if args.len() > 1 {
use tauri::Emitter;
let mut files = Vec::new();
for maybe_file in args.iter().skip(1) {
if maybe_file.starts_with("-") {
continue;
}
let path = PathBuf::from(maybe_file);
if path.extension().and_then(|e| e.to_str()) == Some("ezpplauncher-theme") {
app.fs_scope().allow_file(&path).ok();
files.push(path);
}
}
app.emit("opened", files.clone()).ok();
}
}));
}
let app = builder
.setup(|app| {
log::info!(
target: "launcher",
"EZPPLauncher {} starting on {} ({}) — logs: {}",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH,
logging::logs_directory().display()
);
// ── Config ────────────────────────────────────────────────────────
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".into());
let db_path = PathBuf::from(home).join(".ezpplauncher").join("config.db");
let hwid = get_id().unwrap_or_else(|_| "recorderinsandybridge".into());
let mut cfg = config::Config::new(db_path);
cfg.init(&hwid)?;
// ── Opened files ──────────────────────────────────────────────────
let mut opened_files = Vec::new();
let args = std::env::args().collect::<Vec<_>>();
if args.len() > 1 {
for maybe_file in args.iter().skip(1) {
if maybe_file.starts_with("-") {
continue;
}
let path = PathBuf::from(maybe_file);
if path.extension().and_then(|e| e.to_str()) == Some("ezpplauncher-theme") {
app.fs_scope().allow_file(&path).ok();
opened_files.push(path);
}
}
}
app.manage(AppState {
config: Mutex::new(cfg),
opened_files: Mutex::new(opened_files),
editor_theme_folder: Mutex::new(None),
});
// WebKit's custom-protocol media pipeline is unreliable (and
// loads large WebMs into memory), so Linux and macOS use a real
// loopback HTTP server with byte-range support instead.
#[cfg(any(target_os = "linux", target_os = "macos"))]
match commands::theme_root().and_then(|root| {
let media = theme_server::media_root()?;
theme_server::start(root, media)
}) {
Ok(_) => {}
Err(error) => log::warn!(
target: "launcher",
"Could not start the theme asset server: {error}"
),
}
#[cfg(target_os = "macos")]
if let Some(window) = app.get_webview_window("main") {
if let Err(error) = macos::round_window(&window, 12.0) {
log::warn!(target: "launcher", "Could not round the launcher window: {error}");
}
if let Err(error) = macos::allow_media_autoplay(&window) {
log::warn!(target: "launcher", "{error}");
}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
opened_urls,
get_hwid,
find_osu_installation,
pick_osu_installation,
resolve_osu_folder,
valid_osu_folder,
get_beatmapsets_count,
get_skins_count,
get_osu_version,
get_osu_release_stream,
installation_diagnostics,
set_osu_config_values,
set_osu_user_config_values,
run_osu_updater,
run_osu,
get_osu_skin,
get_ezpp_launcher_update_files,
download_ezpp_launcher_update_files,
replace_ui_files,
is_osu_running,
open_url_in_browser,
get_launcher_version,
exit,
get_platform,
file_size,
check_for_corruption,
presence_connect,
presence_disconnect,
presence_update_status,
presence_update_user,
presence_update_button,
presence_is_connected,
has_osuwinello,
has_wmctrl,
has_net8,
encrypt_string,
download_ezpp_launcher_update,
install_ezpp_launcher_update,
read_theme_info,
backup_theme_folder,
list_theme_backups,
restore_theme_backup,
extract_theme,
download_and_extract_theme,
save_theme_project,
export_theme,
open_theme_folder,
open_theme_editor,
theme_editor_context,
theme_asset_base,
theme_preview_url,
optimize_theme_media,
config_get,
config_set,
config_exists,
config_delete,
config_clear,
config_all,
run_open_tablet_driver,
stop_open_tablet_driver,
is_open_tablet_driver_running,
is_open_tablet_driver_path_valid,
log_frontend,
set_logging_enabled,
open_logs_folder,
logging::list_log_bundles,
logging::read_log_bundle
])
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_cors_fetch::init())
.build(tauri::generate_context!())
.expect("error while building tauri application");
app.run(|app, event| {
// The content view can be replaced again while the webview is being
// built, so re-apply the corner mask once the app is ready.
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Ready = &event {
if let Some(window) = app.get_webview_window("main") {
if let Err(error) = macos::round_window(&window, 12.0) {
log::warn!(target: "launcher", "Could not round the launcher window: {error}");
}
if let Err(error) = macos::allow_media_autoplay(&window) {
log::warn!(target: "launcher", "{error}");
}
}
}
presence::handle_run_event(app, &event);
});
}
-637
View File
@@ -1,637 +0,0 @@
//! Rotating file logs for the launcher, osu! and the patcher.
//!
//! Each stream writes to `~/.ezpplauncher/logs`:
//!
//! - `launcher.log` receives everything, including frontend console output.
//! - `osu.log` receives osu! and its updater, including stdout/stderr.
//! - `patcher.log` receives patcher output.
//!
//! A stream's file is only created when it actually has a line to write, so a
//! launch without osu!/patcher output never leaves empty files behind. When a
//! new session starts, or any stream reaches 5 MiB, all three active files are
//! bundled into one `logs_<date>_<time>.zip` and fresh files are started. The
//! five most recent bundles are kept, so a support report only needs one file.
//! `launcher.log` intentionally includes the osu!/patcher entries too. File
//! logging can be disabled from Settings → Logging; stdout output is unchanged.
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use log::Level;
use serde::Serialize;
use tauri::Runtime;
use tauri::plugin::TauriPlugin;
use tauri_plugin_log::{Target, TargetKind, TimezoneStrategy};
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
use tokio::process::Child;
const MAX_LOG_SIZE: u64 = 5 * 1024 * 1024;
const KEPT_LOG_BUNDLES: usize = 5;
const MAX_LINE_CHARS: usize = 16_000;
static ENABLED: AtomicBool = AtomicBool::new(true);
pub fn set_logging_enabled(enabled: bool) {
ENABLED.store(enabled, Ordering::Relaxed);
}
fn logging_enabled() -> bool {
ENABLED.load(Ordering::Relaxed)
}
/// Read the persisted setting before the config database is initialized, so a
/// disabled logger does not write a startup file before the frontend loads.
fn initial_logging_enabled() -> bool {
let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) else {
return true;
};
let db_path = PathBuf::from(home).join(".ezpplauncher").join("config.db");
if !db_path.exists() {
return true;
}
let Ok(connection) =
rusqlite::Connection::open_with_flags(&db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
else {
return true;
};
let Ok(raw) = connection.query_row(
"SELECT value FROM config WHERE key = 'logging_enabled'",
[],
|row| row.get::<_, String>(0),
) else {
return true;
};
serde_json::from_str::<bool>(&raw).unwrap_or(true)
}
pub fn logs_directory() -> PathBuf {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
home.join(".ezpplauncher").join("logs")
}
#[derive(Clone, Copy)]
enum Stream {
Launcher,
Osu,
Patcher,
}
impl Stream {
const ALL: [Stream; 3] = [Stream::Launcher, Stream::Osu, Stream::Patcher];
fn index(self) -> usize {
match self {
Stream::Launcher => 0,
Stream::Osu => 1,
Stream::Patcher => 2,
}
}
fn file_name(self) -> &'static str {
match self {
Stream::Launcher => "launcher.log",
Stream::Osu => "osu.log",
Stream::Patcher => "patcher.log",
}
}
}
/// Shared state for the three log files. Rotating any stream closes and
/// bundles all of them, so every bundle always holds the same session.
struct LogSet {
dir: PathBuf,
state: Mutex<LogState>,
}
struct LogState {
started: bool,
files: [Option<File>; 3],
sizes: [u64; 3],
}
impl LogSet {
fn new(dir: PathBuf) -> Self {
Self {
dir,
state: Mutex::new(LogState {
started: false,
files: [None, None, None],
sizes: [0; 3],
}),
}
}
fn write(&self, stream: Stream, buf: &[u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if !state.started {
state.started = true;
bundle_active_logs(&self.dir)?;
prune_bundles(&self.dir)?;
}
let index = stream.index();
if state.files[index].is_none() {
self.open_stream(&mut state, stream)?;
}
if state.sizes[index] > 0
&& state.sizes[index].saturating_add(buf.len() as u64) > MAX_LOG_SIZE
{
self.rotate(&mut state, stream)?;
}
let file = state.files[index].as_mut().expect("stream log is open");
file.write_all(buf)?;
state.sizes[index] += buf.len() as u64;
Ok(buf.len())
}
fn flush(&self, stream: Stream) -> io::Result<()> {
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
match state.files[stream.index()].as_mut() {
Some(file) => file.flush(),
None => Ok(()),
}
}
fn open_stream(&self, state: &mut LogState, stream: Stream) -> io::Result<()> {
fs::create_dir_all(&self.dir)?;
let file = OpenOptions::new()
.create(true)
.append(true)
.open(self.dir.join(stream.file_name()))?;
let index = stream.index();
state.sizes[index] = file.metadata()?.len();
state.files[index] = Some(file);
Ok(())
}
fn rotate(&self, state: &mut LogState, stream: Stream) -> io::Result<()> {
for file in state.files.iter_mut() {
if let Some(mut file) = file.take() {
let _ = file.flush();
}
}
bundle_active_logs(&self.dir)?;
prune_bundles(&self.dir)?;
let index = stream.index();
state.sizes[index] = 0;
self.open_stream(state, stream)
}
}
struct SessionWriter {
logs: Arc<LogSet>,
stream: Stream,
}
impl Write for SessionWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if !logging_enabled() {
return Ok(buf.len());
}
self.logs.write(self.stream, buf)
}
fn flush(&mut self) -> io::Result<()> {
if !logging_enabled() {
return Ok(());
}
self.logs.flush(self.stream)
}
}
/// Zip the active logs that have content and remove them afterwards.
fn bundle_active_logs(dir: &Path) -> io::Result<()> {
let mut entries = Vec::new();
for stream in Stream::ALL {
let path = dir.join(stream.file_name());
match path.metadata() {
Ok(metadata) if metadata.len() > 0 => {
entries.push((path, stream.file_name().to_string()));
}
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
}
if entries.is_empty() {
return Ok(());
}
fs::create_dir_all(dir)?;
let bundle_path = unique_bundle_path(dir);
let result = (|| -> io::Result<()> {
let file = File::create(&bundle_path)?;
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
for (path, name) in &entries {
zip.start_file(name.clone(), options).map_err(|error| {
io::Error::other(format!("Could not add {name} to the log bundle: {error}"))
})?;
let mut source = File::open(path)?;
io::copy(&mut source, &mut zip)?;
}
zip.finish().map_err(|error| {
io::Error::other(format!("Could not finish the log bundle: {error}"))
})?;
Ok(())
})();
if let Err(error) = result {
let _ = fs::remove_file(&bundle_path);
return Err(error);
}
for (path, _) in entries {
fs::remove_file(path)?;
}
Ok(())
}
fn unique_bundle_path(dir: &Path) -> PathBuf {
let stamp = archive_timestamp();
let mut path = dir.join(format!("logs_{stamp}.zip"));
let mut suffix = 1;
while path.exists() {
path = dir.join(format!("logs_{stamp}-{suffix}.zip"));
suffix += 1;
}
path
}
fn archive_timestamp() -> String {
let now = time::OffsetDateTime::now_local().unwrap_or_else(|_| time::OffsetDateTime::now_utc());
format!(
"{:04}-{:02}-{:02}_{:02}-{:02}-{:02}",
now.year(),
u8::from(now.month()),
now.day(),
now.hour(),
now.minute(),
now.second()
)
}
fn prune_bundles(dir: &Path) -> io::Result<()> {
let mut bundles = fs::read_dir(dir)?
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
let file_name = path.file_name()?.to_str()?;
if !file_name.starts_with("logs_") || !file_name.ends_with(".zip") {
return None;
}
let modified = entry.metadata().ok()?.modified().ok()?;
Some((path, modified))
})
.collect::<Vec<_>>();
bundles.sort_by_key(|(_, modified)| *modified);
if bundles.len() > KEPT_LOG_BUNDLES {
for (path, _) in bundles.iter().take(bundles.len() - KEPT_LOG_BUNDLES) {
fs::remove_file(path)?;
}
}
Ok(())
}
fn file_target(set: &Arc<LogSet>, stream: Stream) -> Target {
let writer = SessionWriter {
logs: Arc::clone(set),
stream,
};
let dispatch = tauri_plugin_log::fern::Dispatch::new().chain(
tauri_plugin_log::fern::Output::writer(Box::new(writer), "\n"),
);
Target::new(TargetKind::Dispatch(dispatch))
}
/// Build the log plugin. File targets are filtered by logging target so osu!
/// and patcher output stay separate while still being copied to the main log.
pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
set_logging_enabled(initial_logging_enabled());
let logs_dir = logs_directory();
let set = Arc::new(LogSet::new(logs_dir));
let launcher = file_target(&set, Stream::Launcher);
let osu =
file_target(&set, Stream::Osu).filter(|metadata| metadata.target().starts_with("osu"));
let patcher = file_target(&set, Stream::Patcher)
.filter(|metadata| metadata.target().starts_with("patcher"));
tauri_plugin_log::Builder::new()
.targets([Target::new(TargetKind::Stdout), launcher, osu, patcher])
.level(if cfg!(debug_assertions) {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
})
.timezone_strategy(TimezoneStrategy::UseLocal)
.build()
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LogBundleInfo {
pub path: String,
pub name: String,
pub size: u64,
pub modified: u64,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LogBundleEntry {
pub name: String,
pub content: String,
}
/// List the rotating ZIP bundles, newest first, for the in-app log viewer.
#[tauri::command]
pub fn list_log_bundles() -> Result<Vec<LogBundleInfo>, String> {
let dir = logs_directory();
if !dir.is_dir() {
return Ok(Vec::new());
}
let mut bundles = fs::read_dir(&dir)
.map_err(|error| format!("Could not read the logs folder: {error}"))?
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let path = entry.path();
let name = path.file_name()?.to_str()?.to_string();
if !name.starts_with("logs_") || !name.ends_with(".zip") {
return None;
}
let metadata = entry.metadata().ok()?;
let modified = metadata
.modified()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0);
Some(LogBundleInfo {
path: path.to_string_lossy().into_owned(),
name,
size: metadata.len(),
modified,
})
})
.collect::<Vec<_>>();
bundles.sort_by(|a, b| b.modified.cmp(&a.modified));
Ok(bundles)
}
/// Read the launcher/osu/patcher logs out of one bundle.
#[tauri::command]
pub fn read_log_bundle(path: String) -> Result<Vec<LogBundleEntry>, String> {
let dir = logs_directory();
let requested = fs::canonicalize(&path)
.map_err(|error| format!("Could not open the log bundle: {error}"))?;
let root = fs::canonicalize(&dir)
.map_err(|error| format!("Could not resolve the logs folder: {error}"))?;
if !requested.starts_with(&root) {
return Err("The log bundle is outside the logs folder".to_string());
}
let file = File::open(&requested)
.map_err(|error| format!("Could not read the log bundle: {error}"))?;
let mut archive =
zip::ZipArchive::new(file).map_err(|error| format!("Invalid log bundle: {error}"))?;
let mut entries = Vec::new();
for index in 0..archive.len() {
let mut entry = archive
.by_index(index)
.map_err(|error| format!("Could not read a log entry: {error}"))?;
if !entry.name().ends_with(".log") {
continue;
}
let mut bytes = Vec::new();
entry
.by_ref()
.take(2 * 1024 * 1024)
.read_to_end(&mut bytes)
.map_err(|error| format!("Could not read a log entry: {error}"))?;
let mut content = String::from_utf8_lossy(&bytes).into_owned();
if entry.size() > bytes.len() as u64 {
content.push_str("\n… log truncated in the viewer …\n");
}
entries.push(LogBundleEntry {
name: entry.name().to_string(),
content,
});
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
/// Start reading a child's stdout/stderr and write every line to `target`.
pub fn capture_osu_output(child: &mut Child) {
capture_output(child, "osu");
}
/// Start reading a child's stdout/stderr and write every line to `target`.
pub fn capture_patcher_output(child: &mut Child) {
capture_output(child, "patcher");
}
fn capture_output(child: &mut Child, target: &'static str) {
if let Some(stdout) = child.stdout.take() {
stream_lines(stdout, "stdout", target);
}
if let Some(stderr) = child.stderr.take() {
stream_lines(stderr, "stderr", target);
}
}
fn stream_lines<R>(reader: R, stream: &'static str, target: &'static str)
where
R: AsyncRead + Unpin + Send + 'static,
{
tauri::async_runtime::spawn(async move {
let mut lines = BufReader::new(reader).lines();
loop {
match lines.next_line().await {
Ok(Some(line)) => {
let line = truncate(line.trim_end_matches(['\r', '\n']));
if !line.is_empty() {
log::log!(target: target, Level::Info, "[{stream}] {line}");
}
}
Ok(None) => break,
Err(error) => {
log::log!(
target: target,
Level::Warn,
"Could not read {stream} output: {error}"
);
break;
}
}
}
});
}
fn truncate(mut value: &str) -> String {
if value.chars().count() <= MAX_LINE_CHARS {
return value.to_string();
}
value = &value[..value
.char_indices()
.nth(MAX_LINE_CHARS)
.map(|(index, _)| index)
.unwrap_or(value.len())];
format!("{value}…")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn temp_dir(name: &str) -> PathBuf {
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir =
std::env::temp_dir().join(format!("ezpp-logging-{}-{name}-{id}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn write_line(logs: &Arc<LogSet>, stream: Stream, line: &str) {
let mut writer = SessionWriter {
logs: Arc::clone(logs),
stream,
};
writer.write_all(line.as_bytes()).unwrap();
writer.flush().unwrap();
}
#[test]
fn creates_file_only_after_first_line() {
let dir = temp_dir("lazy");
let logs = Arc::new(LogSet::new(dir.clone()));
assert!(!dir.join("osu.log").exists());
write_line(&logs, Stream::Osu, "[stdout] first\n");
assert_eq!(
fs::read_to_string(dir.join("osu.log")).unwrap(),
"[stdout] first\n"
);
assert!(!dir.join("launcher.log").exists());
assert!(!dir.join("patcher.log").exists());
drop(logs);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn bundles_all_streams_when_a_new_session_starts() {
let dir = temp_dir("bundle");
let previous = Arc::new(LogSet::new(dir.clone()));
write_line(&previous, Stream::Launcher, "launcher previous\n");
write_line(&previous, Stream::Osu, "osu previous\n");
write_line(&previous, Stream::Patcher, "patcher previous\n");
drop(previous);
let current = Arc::new(LogSet::new(dir.clone()));
write_line(&current, Stream::Launcher, "launcher current\n");
// Active files only contain the new session.
assert_eq!(
fs::read_to_string(dir.join("launcher.log")).unwrap(),
"launcher current\n"
);
assert!(!dir.join("osu.log").exists());
assert!(!dir.join("patcher.log").exists());
let bundles = fs::read_dir(&dir)
.unwrap()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zip"))
.collect::<Vec<_>>();
assert_eq!(bundles.len(), 1);
let file = File::open(&bundles[0]).unwrap();
let mut zip = zip::ZipArchive::new(file).unwrap();
assert_eq!(zip.len(), 3);
let mut names = (0..zip.len())
.map(|index| zip.by_index(index).unwrap().name().to_string())
.collect::<Vec<_>>();
names.sort();
assert_eq!(names, ["launcher.log", "osu.log", "patcher.log"]);
let mut launcher = String::new();
zip.by_name("launcher.log")
.unwrap()
.read_to_string(&mut launcher)
.unwrap();
assert_eq!(launcher, "launcher previous\n");
drop(zip);
drop(current);
fs::remove_dir_all(dir).unwrap();
}
}
-907
View File
@@ -1,907 +0,0 @@
//! macOS support for osu! stable running inside a Wine wrapper.
//!
//! osu! stable is a Windows application, so on macOS it has to run through a
//! Wine-based wrapper. The launcher supports Wineskin wrappers (the common
//! `osu!.app` wine skin), Whisky bottles and CrossOver bottles.
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{Duration as StdDuration, Instant};
use tokio::process::{Child, Command};
use tokio::time::{Duration, sleep};
use crate::utils::is_valid_osu_folder;
/// Round the borderless launcher window and make its corners transparent.
///
/// macOS does not round undecorated windows on its own, so the webview has to
/// be clipped with the content view's layer. The window itself is made
/// transparent so the area outside that clip shows the desktop instead of a
/// square background.
pub fn round_window(window: &tauri::WebviewWindow, radius: f64) -> Result<(), String> {
use objc2::msg_send;
use objc2::runtime::AnyObject;
let ns_window = window.ns_window().map_err(|error| error.to_string())? as *mut AnyObject;
if ns_window.is_null() {
return Err("The native window is not available".into());
}
unsafe {
let _: () = msg_send![ns_window, setOpaque: false];
let clear: *mut AnyObject = msg_send![objc2::class!(NSColor), clearColor];
let _: () = msg_send![ns_window, setBackgroundColor: clear];
let _: () = msg_send![ns_window, setHasShadow: true];
let content_view: *mut AnyObject = msg_send![ns_window, contentView];
if content_view.is_null() {
return Err("The native content view is not available".into());
}
let _: () = msg_send![content_view, setWantsLayer: true];
let layer: *mut AnyObject = msg_send![content_view, layer];
if layer.is_null() {
return Err("The native content view has no layer".into());
}
let _: () = msg_send![layer, setCornerRadius: radius];
let _: () = msg_send![layer, setMasksToBounds: true];
let _: () = msg_send![ns_window, invalidateShadow];
}
Ok(())
}
/// Allow theme media to start without a click. WKWebView otherwise blocks
/// autoplay for anything with an audio track, which makes a theme's background
/// video sit frozen until the user interacts with the launcher.
pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String> {
use objc2::msg_send;
use objc2::runtime::AnyObject;
window
.with_webview(|webview| unsafe {
let webview = webview.inner() as *mut AnyObject;
if webview.is_null() {
return;
}
let configuration: *mut AnyObject = msg_send![webview, configuration];
if configuration.is_null() {
return;
} // WKAudiovisualMediaTypesNone. This is a public WKWebView setting.
let _: () =
msg_send![configuration, setMediaTypesRequiringUserActionForPlayback: 0usize];
let current: usize = msg_send![configuration, mediaTypesRequiringUserActionForPlayback];
log::debug!(
target: "launcher",
"Theme media autoplay policy: {current}"
);
})
.map_err(|error| format!("Could not enable theme media autoplay: {error}"))
}
/// Show a native open panel that accepts `.app` bundles as well as folders.
///
/// AppKit treats application bundles as packages (directories) by default, so a
/// regular folder picker descends into `osu!.app` instead of selecting it. This
/// panel turns that behaviour off, letting the user pick the wrapper itself,
/// while still allowing a plain Wine prefix folder to be selected.
pub fn pick_osu_installation() -> Result<Option<PathBuf>, String> {
use objc2::msg_send;
use objc2::rc::autoreleasepool;
use objc2::runtime::AnyObject;
use std::ffi::{CStr, CString};
unsafe fn ns_string(value: &str) -> Result<*mut AnyObject, String> {
let value = CString::new(value).map_err(|_| "The path contains a NUL byte".to_string())?;
let string: *mut AnyObject =
unsafe { msg_send![objc2::class!(NSString), stringWithUTF8String: value.as_ptr()] };
if string.is_null() {
return Err("Could not create the native dialog text".to_string());
}
Ok(string)
}
autoreleasepool(|_| unsafe {
let panel: *mut AnyObject = msg_send![objc2::class!(NSOpenPanel), openPanel];
if panel.is_null() {
return Err("Could not create the native file picker".to_string());
}
let _: () = msg_send![panel, setCanChooseFiles: true];
let _: () = msg_send![panel, setCanChooseDirectories: true];
// Keep `.app` bundles selectable instead of navigating into them.
let _: () = msg_send![panel, setTreatsFilePackagesAsDirectories: false];
let _: () = msg_send![panel, setAllowsMultipleSelection: false];
let _: () = msg_send![panel, setCanCreateDirectories: false];
let title = ns_string("Select osu!.app")?;
let _: () = msg_send![panel, setTitle: title];
let message = ns_string("Pick your osu!.app wrapper, or a Wine prefix folder.")?;
let _: () = msg_send![panel, setMessage: message];
let prompt = ns_string("Select")?;
let _: () = msg_send![panel, setPrompt: prompt];
let applications = ns_string("/Applications")?;
let directory_url: *mut AnyObject =
msg_send![objc2::class!(NSURL), fileURLWithPath: applications];
if !directory_url.is_null() {
let _: () = msg_send![panel, setDirectoryURL: directory_url];
}
// NSModalResponseOK
let response: i64 = msg_send![panel, runModal];
if response != 1 {
return Ok(None);
}
let url: *mut AnyObject = msg_send![panel, URL];
if url.is_null() {
return Ok(None);
}
let path: *mut AnyObject = msg_send![url, path];
if path.is_null() {
return Ok(None);
}
let utf8: *const std::os::raw::c_char = msg_send![path, UTF8String];
if utf8.is_null() {
return Ok(None);
}
let path = CStr::from_ptr(utf8).to_string_lossy().into_owned();
Ok(Some(PathBuf::from(path)))
})
}
pub struct WineRuntime {
prefix: PathBuf,
wine_bin: PathBuf,
wine_root: Option<PathBuf>,
}
#[derive(Clone)]
pub struct WineDiagnostics {
pub prefix: PathBuf,
pub wine_bin: PathBuf,
pub app_bundle: Option<PathBuf>,
pub net8_installed: bool,
pub wine_version: Option<String>,
pub prefix_arch: Option<String>,
}
impl WineDiagnostics {
/// Inspect the wrapper without starting it, for the launcher's diagnostics panel.
pub fn for_osu_folder(osu_folder: &Path) -> Result<Self, String> {
let runtime = WineRuntime::for_osu_folder(osu_folder)?;
let net8_installed = [
runtime
.prefix
.join("drive_c/Program Files/dotnet/shared/Microsoft.WindowsDesktop.App"),
runtime
.prefix
.join("drive_c/Program Files (x86)/dotnet/shared/Microsoft.WindowsDesktop.App"),
]
.iter()
.any(|path| {
fs::read_dir(path)
.ok()
.map(|entries| {
entries.filter_map(|entry| entry.ok()).any(|entry| {
entry
.file_name()
.to_str()
.map(|name| name.starts_with("8."))
.unwrap_or(false)
})
})
.unwrap_or(false)
});
Ok(Self {
prefix: runtime.prefix.clone(),
wine_bin: runtime.wine_bin.clone(),
app_bundle: app_bundle_for(&runtime.prefix),
net8_installed,
wine_version: wine_version_for(&runtime.wine_bin),
prefix_arch: prefix_architecture(&runtime.prefix),
})
}
}
impl WineRuntime {
/// Build a runtime description for an osu! folder inside a Wine prefix.
pub fn for_osu_folder(osu_folder: &Path) -> Result<Self, String> {
if !osu_folder.join("osu!.exe").is_file() {
return Err(format!(
"osu!.exe was not found in {}",
osu_folder.display()
));
}
let prefix = prefix_for(osu_folder).ok_or_else(|| {
"This osu! installation is not inside a Wine prefix. On macOS, choose the osu! folder inside your Wineskin, Whisky or CrossOver wrapper.".to_string()
})?;
let app = app_bundle_for(&prefix);
let (wine_bin, wine_root) = find_wine_bin(&prefix, app.as_deref()).ok_or_else(|| {
"Could not find a Wine runtime for this osu! installation. Make sure the Wineskin/Whisky/CrossOver wrapper is still installed.".to_string()
})?;
Ok(Self {
prefix,
wine_bin,
wine_root,
})
}
/// Spawn a Windows executable through the wrapper's Wine runtime.
pub fn spawn(&self, exe: &Path, args: &[&str]) -> Result<Child, String> {
let loader = self.loader_for(exe);
if !loader.is_file() {
return Err(format!(
"Wine loader not found at {}. The wrapper may be incomplete.",
loader.display()
));
}
let mut command = Command::new(&loader);
command.arg(exe).args(args);
command.current_dir(exe.parent().unwrap_or(&self.prefix));
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.env("WINEPREFIX", &self.prefix);
command.env("WINEDEBUG", "-all");
command.env("WINEDLLOVERRIDES", "mscoree,mshtml=");
command.env("FREETYPE_PROPERTIES", "truetype:interpreter-version=35");
if let Some(wine_root) = &self.wine_root {
let mut library_paths = vec![
wine_root.join("lib"),
wine_root.join("lib64"),
wine_root.join("lib32on64"),
wine_root.clone(),
PathBuf::from("/opt/local/lib"),
PathBuf::from("/usr/lib"),
PathBuf::from("/usr/libexec"),
PathBuf::from("/usr/lib/system"),
PathBuf::from("/opt/X11/lib"),
];
if let Some(existing) = std::env::var_os("DYLD_FALLBACK_LIBRARY_PATH") {
library_paths.extend(std::env::split_paths(&existing));
}
if let Ok(library_path) = std::env::join_paths(library_paths) {
command.env("DYLD_FALLBACK_LIBRARY_PATH", &library_path);
command.env("WINESKIN_LIB_PATH_FOR_FALLBACK", &library_path);
}
}
let mut paths = vec![self.wine_bin.clone()];
if let Some(existing) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&existing));
}
if let Ok(path) = std::env::join_paths(paths) {
command.env("PATH", path);
}
let executable_name = exe
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "the Windows application".to_string());
command.spawn().map_err(|error| {
format!(
"Failed to start {executable_name} through Wine ({}): {}",
loader.display(),
error
)
})
}
/// Stop every process that belongs to this Wine prefix.
pub async fn kill_prefix(&self) {
let wineserver = self.wine_bin.join("wineserver");
if !wineserver.is_file() {
return;
}
let _ = Command::new(wineserver)
.arg("-k")
.env("WINEPREFIX", &self.prefix)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
}
fn loader_for(&self, exe: &Path) -> PathBuf {
let is_64_bit = pe_machine(exe) == Some(0x8664);
let candidates: &[&str] = if is_64_bit {
&["wine64", "wine"]
} else {
&["wine32on64", "wine", "wine64"]
};
candidates
.iter()
.map(|candidate| self.wine_bin.join(candidate))
.find(|candidate| candidate.is_file())
.unwrap_or_else(|| self.wine_bin.join(candidates[0]))
}
}
/// Run osu! (`-devserver ez-pp.farm`) and wait until the game exits.
pub async fn run_osu(osu_folder: &str, patch: bool) -> Result<(), String> {
let folder = Path::new(osu_folder);
let runtime = WineRuntime::for_osu_folder(folder).map_err(|error| {
log::error!(target: "osu", "Could not prepare the Wine runtime: {error}");
error
})?;
let mut game = runtime
.spawn(&folder.join("osu!.exe"), &["-devserver", "ez-pp.farm"])
.map_err(|error| {
log::error!(target: "osu", "Could not start osu!: {error}");
error
})?;
if let Some(pid) = game.id() {
log::info!(target: "osu", "osu! started through Wine (pid {pid})");
}
crate::logging::capture_osu_output(&mut game);
if patch {
if let Err(error) = start_patcher(&runtime, folder).await {
log::warn!(target: "patcher", "Could not start the patcher: {error}");
}
}
match game.wait().await {
Ok(status) => log::info!(target: "osu", "osu! exited with {status}"),
Err(error) => {
log::warn!(target: "osu", "Could not wait for osu!: {error}");
return Err(format!("Failed to wait for osu!: {}", error));
}
}
Ok(())
}
/// Start the Windows patcher through the same Wine prefix as osu!.
async fn start_patcher(runtime: &WineRuntime, osu_folder: &Path) -> Result<(), String> {
let patcher_exe = osu_folder
.join("EZPPLauncher")
.join("patcher")
.join("osu!.patcher.exe");
if !patcher_exe.is_file() {
return Err(format!(
"Patcher was requested but {} does not exist",
patcher_exe.display()
));
}
wait_for_osu_game().await;
log::info!(
target: "patcher",
"Starting the patcher from {}",
patcher_exe.display()
);
let mut patcher = runtime.spawn(&patcher_exe, &[])?;
if let Some(pid) = patcher.id() {
log::info!(target: "patcher", "Patcher started through Wine (pid {pid})");
}
crate::logging::capture_patcher_output(&mut patcher);
tauri::async_runtime::spawn(async move {
match patcher.wait().await {
Ok(status) => log::info!(target: "patcher", "Patcher exited with {status}"),
Err(error) => log::warn!(target: "patcher", "Could not wait for the patcher: {error}"),
}
});
Ok(())
}
/// Wait until the Wine-hosted osu! process exists, then give it time to create
/// its game window. Wine does not expose the window title to the launcher here,
/// so the process is the best signal available.
async fn wait_for_osu_game() {
let mut system = sysinfo::System::new_all();
let deadline = Instant::now() + StdDuration::from_secs(30);
let mut detected = false;
while Instant::now() < deadline {
system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
if system
.processes()
.values()
.any(crate::utils::is_osu_game_process)
{
detected = true;
break;
}
sleep(Duration::from_millis(500)).await;
}
if !detected {
log::warn!(
target: "patcher",
"Could not detect the osu! process; starting the patcher anyway"
);
}
// Wine gives no reliable window-ready signal, so leave the game a moment
// to finish initializing before the patcher attaches to it.
sleep(Duration::from_secs(8)).await;
}
/// Run `osu!.exe -repair`, wait for the updater and close the game it starts.
pub async fn run_osu_updater(osu_folder: &str) -> Result<(), String> {
let folder = Path::new(osu_folder);
let runtime = WineRuntime::for_osu_folder(folder).map_err(|error| {
log::error!(target: "osu", "Could not prepare the Wine runtime: {error}");
error
})?;
let mut updater = runtime
.spawn(&folder.join("osu!.exe"), &["-repair"])
.map_err(|error| {
log::error!(target: "osu", "Could not start the osu! updater: {error}");
error
})?;
if let Some(pid) = updater.id() {
log::info!(target: "osu", "osu! updater started through Wine (pid {pid})");
}
crate::logging::capture_osu_output(&mut updater);
let mut system = sysinfo::System::new_all();
let deadline = Instant::now() + StdDuration::from_secs(300);
loop {
if updater
.try_wait()
.map_err(|error| format!("Failed to check the osu! updater: {}", error))?
.is_some()
{
log::info!(target: "osu", "The osu! updater exited");
break;
}
system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
if system
.processes()
.values()
.any(crate::utils::is_osu_game_process)
{
// The updater relaunched osu! to finish the update. Close it so the
// launcher can start the game itself with the correct server.
log::info!(target: "osu", "The updater relaunched osu!; closing the Wine prefix");
runtime.kill_prefix().await;
break;
}
if Instant::now() >= deadline {
log::warn!(target: "osu", "Timed out waiting for the osu! updater; closing the Wine prefix");
runtime.kill_prefix().await;
break;
}
sleep(Duration::from_millis(500)).await;
}
match updater.wait().await {
Ok(status) => log::info!(target: "osu", "osu! updater exited with {status}"),
Err(error) => log::warn!(target: "osu", "Could not wait for the osu! updater: {error}"),
}
Ok(())
}
/// Resolve a user-selected path (osu! folder, `.app` bundle, prefix or drive_c)
/// to a valid osu! installation folder.
pub fn resolve_osu_folder(folder: &Path) -> Option<PathBuf> {
if is_valid_osu_folder(folder) {
return Some(folder.to_path_buf());
}
let mut prefixes = Vec::new();
if folder.file_name() == Some(OsStr::new("drive_c")) {
if let Some(parent) = canonical_prefix(folder) {
prefixes.push(parent);
}
}
if folder.join("drive_c").is_dir() {
if let Some(parent) = canonical_prefix(&folder.join("drive_c")) {
prefixes.push(parent);
}
}
if folder.extension() == Some(OsStr::new("app")) || folder.join("Contents").is_dir() {
for drive_c in [
folder.join("Contents/Resources/drive_c"),
folder.join("Contents/SharedSupport/drive_c"),
folder.join("Contents/SharedSupport/prefix/drive_c"),
] {
if drive_c.is_dir() {
if let Some(parent) = canonical_prefix(&drive_c) {
prefixes.push(parent);
}
}
}
let mut drive_dirs = Vec::new();
find_drive_c_dirs(folder, 5, &mut drive_dirs);
prefixes.extend(
drive_dirs
.iter()
.filter_map(|drive_c| drive_c.parent().map(Path::to_path_buf)),
);
}
if let Some(prefix) = prefix_for(folder) {
prefixes.push(prefix);
}
prefixes.sort();
prefixes.dedup();
prefixes
.into_iter()
.find_map(|prefix| find_osu_folder_in_prefix(&prefix))
}
/// Auto-detect an osu! installation in the usual macOS wrapper locations.
pub fn find_osu_installation() -> Option<String> {
for app in app_bundles() {
if let Some(folder) = resolve_osu_folder(&app) {
return Some(folder.to_string_lossy().into_owned());
}
}
for prefix in bottle_prefixes() {
if let Some(folder) = find_osu_folder_in_prefix(&prefix) {
return Some(folder.to_string_lossy().into_owned());
}
}
None
}
fn find_osu_folder_in_prefix(prefix: &Path) -> Option<PathBuf> {
let drive_c = prefix.join("drive_c");
if !drive_c.is_dir() {
return None;
}
if let Some(folder) = common_osu_folders(&drive_c)
.into_iter()
.find(|folder| is_valid_osu_folder(folder))
{
return Some(folder);
}
let mut candidates = Vec::new();
collect_osu_folders(&drive_c, 0, 6, &mut candidates);
candidates
.into_iter()
.find(|folder| is_valid_osu_folder(folder))
}
fn common_osu_folders(drive_c: &Path) -> Vec<PathBuf> {
let mut folders = vec![
drive_c.join("osu!"),
drive_c.join("Program Files").join("osu!"),
drive_c.join("Program Files (x86)").join("osu!"),
];
if let Ok(users) = fs::read_dir(drive_c.join("users")) {
for user in users.flatten() {
let user_path = user.path();
if user_path.is_dir() {
folders.push(user_path.join("AppData").join("Local").join("osu!"));
folders.push(user_path.join("AppData").join("Roaming").join("osu!"));
}
}
}
folders
}
fn collect_osu_folders(dir: &Path, depth: usize, max_depth: usize, out: &mut Vec<PathBuf>) {
if depth > max_depth {
return;
}
if dir.join("osu!.exe").is_file() {
out.push(dir.to_path_buf());
return;
}
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(metadata) = fs::symlink_metadata(&path) else {
continue;
};
if metadata.file_type().is_symlink() || !metadata.is_dir() {
continue;
}
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_ascii_lowercase();
if matches!(
name.as_str(),
"windows" | "programdata" | "$recycle.bin" | "system volume information"
) {
continue;
}
collect_osu_folders(&path, depth + 1, max_depth, out);
}
}
fn find_drive_c_dirs(dir: &Path, depth: usize, out: &mut Vec<PathBuf>) {
if depth == 0 {
return;
}
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(metadata) = fs::symlink_metadata(&path) else {
continue;
};
if metadata.file_type().is_symlink() {
// Wineskin wrappers often add a convenience drive_c symlink at
// the app root. Resolve it to the real prefix so WINEPREFIX points
// at Contents/Resources instead of the bundle itself.
if path.file_name() == Some(OsStr::new("drive_c")) {
if let Ok(canonical) = fs::canonicalize(&path) {
out.push(canonical);
}
}
continue;
}
if !metadata.is_dir() {
continue;
}
if path.file_name() == Some(OsStr::new("drive_c")) {
out.push(path);
continue;
}
// Do not descend into nested apps (for example Wineskin.app).
if path.extension() == Some(OsStr::new("app")) {
continue;
}
find_drive_c_dirs(&path, depth - 1, out);
}
}
fn prefix_for(osu_folder: &Path) -> Option<PathBuf> {
let drive_c = osu_folder
.ancestors()
.find(|ancestor| ancestor.file_name() == Some(OsStr::new("drive_c")))?;
canonical_prefix(drive_c)
}
fn canonical_prefix(drive_c: &Path) -> Option<PathBuf> {
let canonical = fs::canonicalize(drive_c).ok()?;
canonical.parent().map(Path::to_path_buf)
}
fn app_bundle_for(prefix: &Path) -> Option<PathBuf> {
prefix
.ancestors()
.find(|ancestor| ancestor.extension() == Some(OsStr::new("app")))
.map(Path::to_path_buf)
}
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
fn app_bundles() -> Vec<PathBuf> {
let mut roots = vec![PathBuf::from("/Applications")];
if let Some(home) = home_dir() {
roots.push(home.join("Applications"));
}
let mut apps = Vec::new();
for root in roots {
let Ok(entries) = fs::read_dir(root) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension() == Some(OsStr::new("app")) {
apps.push(path);
}
}
}
apps.sort();
apps
}
fn bottle_prefixes() -> Vec<PathBuf> {
let Some(home) = home_dir() else {
return Vec::new();
};
let bottle_roots = [
home.join("Library/Containers/com.isaacmarovitz.Whisky/Bottles"),
home.join("Library/Application Support/CrossOver/Bottles"),
home.join("Library/Application Support/Whisky/Bottles"),
];
let mut prefixes = Vec::new();
for root in bottle_roots {
let Ok(entries) = fs::read_dir(root) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.join("drive_c").is_dir() {
prefixes.push(path);
}
}
}
prefixes.sort();
prefixes
}
fn find_wine_bin(prefix: &Path, app: Option<&Path>) -> Option<(PathBuf, Option<PathBuf>)> {
let mut candidates: Vec<(PathBuf, Option<PathBuf>)> = Vec::new();
if let Some(app) = app {
for root in [
app.join("Contents/SharedSupport/wine"),
app.join("Contents/Resources/WineskinEngine.bundle/Wine"),
app.join("Contents/Frameworks/wswine.bundle"),
] {
push_wine_root(&mut candidates, root);
}
}
for root in [
prefix.join("WineskinEngine.bundle/Wine"),
prefix.join("Wine"),
] {
push_wine_root(&mut candidates, root);
}
if let Some(home) = home_dir() {
for root in [
home.join("Applications/Whisky.app/Contents/Resources/wine"),
home.join("Applications/Whisky.app/Contents/Libraries/Wine"),
] {
push_wine_root(&mut candidates, root);
}
}
for root in [
PathBuf::from("/Applications/Whisky.app/Contents/Resources/wine"),
PathBuf::from("/Applications/Whisky.app/Contents/Libraries/Wine"),
PathBuf::from("/Applications/CrossOver.app/Contents/SharedSupport/CrossOver"),
] {
push_wine_root(&mut candidates, root);
}
for bin in [
PathBuf::from("/opt/homebrew/bin"),
PathBuf::from("/usr/local/bin"),
] {
if has_wine_loader(&bin) {
candidates.push((bin, None));
}
}
for binary in ["wine64", "wine32on64", "wine"] {
if let Some(path) = find_in_path(binary) {
if let Some(bin) = path.parent() {
candidates.push((bin.to_path_buf(), None));
break;
}
}
}
candidates.into_iter().find(|(bin, _)| has_wine_loader(bin))
}
fn push_wine_root(candidates: &mut Vec<(PathBuf, Option<PathBuf>)>, root: PathBuf) {
let bin = root.join("bin");
if has_wine_loader(&bin) {
candidates.push((bin, Some(root)));
}
}
fn has_wine_loader(bin: &Path) -> bool {
["wine32on64", "wine64", "wine"]
.iter()
.any(|loader| bin.join(loader).is_file())
}
fn wine_version_for(bin: &Path) -> Option<String> {
let loader = ["wine64", "wine", "wine32on64"]
.iter()
.map(|name| bin.join(name))
.find(|path| path.is_file())?;
let output = std::process::Command::new(loader)
.arg("--version")
.env("WINEDEBUG", "-all")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
(!version.is_empty()).then_some(version)
}
fn prefix_architecture(prefix: &Path) -> Option<String> {
if prefix.join("drive_c/windows/syswow64").is_dir() {
Some("win64".to_string())
} else if prefix.join("drive_c/windows/system32").is_dir() {
Some("win32".to_string())
} else {
None
}
}
fn find_in_path(binary: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|dir| dir.join(binary))
.find(|candidate| candidate.is_file())
}
fn pe_machine(exe: &Path) -> Option<u16> {
use std::io::{Read, Seek, SeekFrom};
let mut file = fs::File::open(exe).ok()?;
let mut signature = [0u8; 2];
file.read_exact(&mut signature).ok()?;
if &signature != b"MZ" {
return None;
}
file.seek(SeekFrom::Start(0x3c)).ok()?;
let mut offset = [0u8; 4];
file.read_exact(&mut offset).ok()?;
file.seek(SeekFrom::Start(u32::from_le_bytes(offset) as u64))
.ok()?;
let mut header = [0u8; 6];
file.read_exact(&mut header).ok()?;
if &header[..4] != b"PE\0\0" {
return None;
}
Some(u16::from_le_bytes([header[4], header[5]]))
}
-6
View File
@@ -1,6 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
ezpplauncher_lib::run()
}
-370
View File
@@ -1,370 +0,0 @@
use std::io::{Cursor, Read};
use anyhow::{Result, anyhow};
use byteorder::{LittleEndian, ReadBytesExt};
use std::collections::HashMap;
pub struct OsuReader {
cursor: Cursor<Vec<u8>>,
}
impl OsuReader {
pub fn new(data: Vec<u8>) -> Self {
Self {
cursor: Cursor::new(data),
}
}
fn read_i16(&mut self) -> Result<i16> {
Ok(self.cursor.read_i16::<LittleEndian>()?)
}
fn read_i32(&mut self) -> Result<i32> {
Ok(self.cursor.read_i32::<LittleEndian>()?)
}
fn read_i64(&mut self) -> Result<i64> {
Ok(self.cursor.read_i64::<LittleEndian>()?)
}
fn read_u8(&mut self) -> Result<u8> {
Ok(self.cursor.read_u8()?)
}
fn read_f32(&mut self) -> Result<f32> {
Ok(self.cursor.read_f32::<LittleEndian>()?)
}
fn read_f64(&mut self) -> Result<f64> {
Ok(self.cursor.read_f64::<LittleEndian>()?)
}
fn read_bool(&mut self) -> Result<bool> {
Ok(self.read_u8()? != 0)
}
fn read_string(&mut self) -> Result<Option<String>> {
let indicator = self.read_u8()?;
if indicator == 0 {
return Ok(None);
}
if indicator != 0x0B {
return Err(anyhow!("Invalid osu string indicator"));
}
let len = self.read_uleb128()? as usize;
let mut buf = vec![0; len];
self.cursor.read_exact(&mut buf)?;
Ok(Some(String::from_utf8(buf)?))
}
fn read_uleb128(&mut self) -> Result<u64> {
let mut result = 0;
let mut shift = 0;
loop {
let byte = self.read_u8()?;
result |= ((byte & 0x7F) as u64) << shift;
if (byte & 0x80) == 0 {
break;
}
shift += 7;
}
Ok(result)
}
}
#[derive(Debug)]
pub struct TimingPoint {
pub bpm: f64,
pub offset: f64,
pub inherited: bool,
}
#[derive(Debug)]
pub struct Beatmap {
pub artist_name: Option<String>,
pub artist_unicode: Option<String>,
pub song_title: Option<String>,
pub song_title_unicode: Option<String>,
pub creator: Option<String>,
pub difficulty: Option<String>,
pub audio: Option<String>,
pub md5: Option<String>,
pub osu_file_name: Option<String>,
pub ranked_status: u8,
pub hitcircles: i16,
pub slider_count: i16,
pub spinner_count: i16,
pub last_modification: i64,
pub approach_rate: f32,
pub circle_size: f32,
pub hp_drain: f32,
pub overall_difficulty: f32,
pub slider_velocity: f64,
pub difficulties: HashMap<i32, f64>,
pub drain_time: i32,
pub total_time: i32,
pub preview_offset: i32,
pub beatmap_id: i32,
pub beatmapset_id: i32,
pub thread_id: i32,
pub grade_standard: u8,
pub grade_taiko: u8,
pub grade_catch: u8,
pub grade_mania: u8,
pub local_beatmap_offset: i16,
pub stack_leniency: f32,
pub mode: u8,
pub song_source: Option<String>,
pub song_tags: Option<String>,
pub online_beatmap_offset: i16,
pub title_font: Option<String>,
pub unplayed: bool,
pub last_played: i64,
pub osz2: bool,
pub folder_name: Option<String>,
pub last_checked_against_repository: i64,
pub ignore_sound: bool,
pub ignore_skin: bool,
pub disable_storyboard: bool,
pub disable_video: bool,
pub visual_override: bool,
pub last_modification_time: i32,
pub mania_scroll_speed: u8,
pub timing_points: Vec<TimingPoint>,
}
#[derive(Debug)]
pub struct OsuDbData {
pub osuver: i32,
pub folder_count: i32,
pub is_unlocked: bool,
pub date_unlock_ticks: i64,
pub username: Option<String>,
pub beatmaps: Vec<Beatmap>,
pub userperms: i32,
}
//
// =========================
// PARSER
// =========================
//
pub fn parse_osudb(bytes: Vec<u8>) -> Result<OsuDbData> {
let mut reader = OsuReader::new(bytes);
let osuver = reader.read_i32()?;
let folder_count = reader.read_i32()?;
let is_unlocked = reader.read_bool()?;
let date_unlock_ticks = reader.read_i64()?;
let username = reader.read_string()?;
let beatmaps_count = reader.read_i32()?;
let mut beatmaps = Vec::new();
for _ in 0..beatmaps_count {
if osuver < 20191107 {
reader.read_i32()?; // entry size
}
let artist_name = reader.read_string()?;
let artist_unicode = reader.read_string()?;
let song_title = reader.read_string()?;
let song_title_unicode = reader.read_string()?;
let creator = reader.read_string()?;
let difficulty = reader.read_string()?;
let audio = reader.read_string()?;
let md5 = reader.read_string()?;
let osu_file_name = reader.read_string()?;
let ranked_status = reader.read_u8()?;
let hitcircles = reader.read_i16()?;
let slider_count = reader.read_i16()?;
let spinner_count = reader.read_i16()?;
let last_modification = reader.read_i64()?;
let approach_rate = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let circle_size = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let hp_drain = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let overall_difficulty = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let slider_velocity = reader.read_f64()?;
let mut difficulties: HashMap<i32, f64> = HashMap::new();
if osuver >= 20140609 {
for _ in 0..4 {
let len = reader.read_i32()?;
for _ in 0..len {
reader.read_u8()?;
let mode = reader.read_i32()?;
reader.read_u8()?;
let diff = if osuver > 20250107 {
reader.read_f32()? as f64
} else {
reader.read_f64()?
};
difficulties.insert(mode, diff);
}
}
}
let drain_time = reader.read_i32()?;
let total_time = reader.read_i32()?;
let preview_offset = reader.read_i32()?;
let timing_len = reader.read_i32()?;
let mut timing_points = Vec::new();
for _ in 0..timing_len {
timing_points.push(TimingPoint {
bpm: reader.read_f64()?,
offset: reader.read_f64()?,
inherited: reader.read_bool()?,
});
}
let beatmap_id = reader.read_i32()?;
let beatmapset_id = reader.read_i32()?;
let thread_id = reader.read_i32()?;
let grade_standard = reader.read_u8()?;
let grade_taiko = reader.read_u8()?;
let grade_catch = reader.read_u8()?;
let grade_mania = reader.read_u8()?;
let local_beatmap_offset = reader.read_i16()?;
let stack_leniency = reader.read_f32()?;
let mode = reader.read_u8()?;
let song_source = reader.read_string()?;
let song_tags = reader.read_string()?;
let online_beatmap_offset = reader.read_i16()?;
let title_font = reader.read_string()?;
let unplayed = reader.read_bool()?;
let last_played = reader.read_i64()?;
let osz2 = reader.read_bool()?;
let folder_name = reader.read_string()?;
let last_checked_against_repository = reader.read_i64()?;
let ignore_sound = reader.read_bool()?;
let ignore_skin = reader.read_bool()?;
let disable_storyboard = reader.read_bool()?;
let disable_video = reader.read_bool()?;
let visual_override = reader.read_bool()?;
if osuver < 20140609 {
reader.read_i16()?;
}
let last_modification_time = reader.read_i32()?;
let mania_scroll_speed = reader.read_u8()?;
beatmaps.push(Beatmap {
artist_name,
artist_unicode,
song_title,
song_title_unicode,
creator,
difficulty,
audio,
md5,
osu_file_name,
ranked_status,
hitcircles,
slider_count,
spinner_count,
last_modification,
approach_rate,
circle_size,
hp_drain,
overall_difficulty,
slider_velocity,
difficulties,
drain_time,
total_time,
preview_offset,
beatmap_id,
beatmapset_id,
thread_id,
grade_standard,
grade_taiko,
grade_catch,
grade_mania,
local_beatmap_offset,
stack_leniency,
mode,
song_source,
song_tags,
online_beatmap_offset,
title_font,
unplayed,
last_played,
osz2,
folder_name,
last_checked_against_repository,
ignore_sound,
ignore_skin,
disable_storyboard,
disable_video,
visual_override,
last_modification_time,
mania_scroll_speed,
timing_points,
});
}
let userperms = reader.read_i32()?;
Ok(OsuDbData {
osuver,
folder_count,
is_unlocked,
date_unlock_ticks,
username,
beatmaps,
userperms,
})
}
-359
View File
@@ -1,359 +0,0 @@
use once_cell::sync::Lazy;
use presenceforge::{ActivityBuilder, AsyncDiscordIpcClient};
use std::sync::Mutex as StdMutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, RunEvent};
use tokio::sync::{mpsc, oneshot};
use tokio::time::{Duration, interval};
#[derive(Clone, Debug)]
pub struct PresenceButton {
pub label: String,
pub url: String,
}
#[derive(Clone, Debug)]
pub struct PresenceData {
pub state: String,
pub details: String,
pub large_image_key: String,
pub large_image_text: String,
pub small_image_key: Option<String>,
pub small_image_text: Option<String>,
pub dynamic_button: Option<PresenceButton>,
}
impl Default for PresenceData {
fn default() -> Self {
Self {
state: "Idle in Launcher...".to_string(),
details: " ".to_string(),
large_image_key: "ezppfarm".to_string(),
large_image_text: "EZPPFarm".to_string(),
small_image_key: None,
small_image_text: None,
dynamic_button: None,
}
}
}
#[derive(Debug)]
enum PresenceCommand {
Connect(oneshot::Sender<bool>),
Disconnect(oneshot::Sender<()>),
UpdateData(PresenceData),
IsConnected(oneshot::Sender<bool>),
}
enum ReconnectResult {
Connected(AsyncDiscordIpcClient),
Failed,
}
struct PresenceActor {
receiver: mpsc::Receiver<PresenceCommand>,
reconnect_rx: mpsc::Receiver<ReconnectResult>,
reconnect_tx: mpsc::Sender<ReconnectResult>,
client: Option<AsyncDiscordIpcClient>,
data: PresenceData,
start_timestamp: i64,
reconnecting: bool,
}
impl PresenceActor {
fn new(receiver: mpsc::Receiver<PresenceCommand>) -> Self {
let start = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let (reconnect_tx, reconnect_rx) = mpsc::channel(1);
Self {
receiver,
reconnect_rx,
reconnect_tx,
client: None,
data: PresenceData::default(),
start_timestamp: start,
reconnecting: false,
}
}
async fn run(&mut self) {
let mut heartbeat = interval(Duration::from_secs(5));
heartbeat.tick().await;
loop {
tokio::select! {
Some(cmd) = self.receiver.recv() => {
match cmd {
PresenceCommand::Connect(resp) => self.cmd_connect(resp).await,
PresenceCommand::Disconnect(resp) => self.cmd_disconnect(resp).await,
PresenceCommand::UpdateData(data) => {
self.data = data;
self.push_presence().await;
}
PresenceCommand::IsConnected(resp) => {
let _ = resp.send(self.client.is_some());
}
}
}
Some(result) = self.reconnect_rx.recv() => {
self.reconnecting = false;
match result {
ReconnectResult::Connected(client) => {
log::info!(target: "launcher", "Presence: reconnect succeeded");
self.client = Some(client);
self.push_presence().await;
}
ReconnectResult::Failed => {
log::warn!(target: "launcher", "Presence: all reconnect attempts exhausted");
}
}
}
_ = heartbeat.tick() => {
self.push_presence().await;
}
}
}
}
async fn cmd_connect(&mut self, resp: oneshot::Sender<bool>) {
if self.client.is_some() {
let _ = resp.send(true);
return;
}
match try_connect_once().await {
Some(client) => {
self.client = Some(client);
self.reconnecting = false;
self.push_presence().await;
let _ = resp.send(true);
}
None => {
log::warn!(
target: "launcher",
"Presence: initial connect failed; spawning reconnect task"
);
let _ = resp.send(false);
self.spawn_reconnect_task();
}
}
}
async fn cmd_disconnect(&mut self, resp: oneshot::Sender<()>) {
if let Some(mut client) = self.client.take() {
let _ = client.clear_activity().await;
log::info!(target: "launcher", "Presence: disconnected");
}
let _ = resp.send(());
}
async fn push_presence(&mut self) {
let client = match self.client.as_mut() {
Some(c) => c,
None => return,
};
let mut builder = ActivityBuilder::new()
.state(&self.data.state)
.details(&self.data.details)
.start_timestamp(self.start_timestamp as u64)
.large_image(&self.data.large_image_key)
.large_text(&self.data.large_image_text);
if let Some(key) = &self.data.small_image_key {
builder = builder.small_image(key);
}
if let Some(text) = &self.data.small_image_text {
builder = builder.small_text(text);
}
if let Some(btn) = &self.data.dynamic_button {
builder = builder.button(&btn.label, &btn.url);
} else {
builder = builder.button(
"Download the Launcher",
"https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest",
);
}
builder = builder.button("Join EZPPFarm", "https://ez-pp.farm/discord");
let activity = builder.build();
if let Err(e) = client.set_activity(&activity).await {
log::warn!(
target: "launcher",
"Presence: set_activity failed ({:?}); will reconnect",
e
);
self.client = None;
self.spawn_reconnect_task();
}
}
fn spawn_reconnect_task(&mut self) {
if self.reconnecting {
return;
}
self.reconnecting = true;
let tx = self.reconnect_tx.clone();
tokio::spawn(async move {
const MAX_ATTEMPTS: u32 = 5;
for attempt in 1..=MAX_ATTEMPTS {
let delay = Duration::from_secs(5 * attempt as u64);
log::info!(
target: "launcher",
"Presence: reconnect attempt {}/{} in {:?}…",
attempt,
MAX_ATTEMPTS,
delay
);
tokio::time::sleep(delay).await;
if let Some(client) = try_connect_once().await {
let _ = tx.send(ReconnectResult::Connected(client)).await;
return;
}
}
let _ = tx.send(ReconnectResult::Failed).await;
});
}
}
async fn try_connect_once() -> Option<AsyncDiscordIpcClient> {
match AsyncDiscordIpcClient::new("1032772293220384808").await {
Ok(mut client) => match client.connect().await {
Ok(_) => {
log::info!(target: "launcher", "Presence: IPC connected");
Some(client)
}
Err(e) => {
log::warn!(target: "launcher", "Presence: IPC connect error: {:?}", e);
None
}
},
Err(e) => {
log::warn!(target: "launcher", "Presence: failed to create client: {:?}", e);
None
}
}
}
static PRESENCE_TX: Lazy<mpsc::Sender<PresenceCommand>> = Lazy::new(|| {
let (tx, rx) = mpsc::channel(32);
let mut actor = PresenceActor::new(rx);
tokio::spawn(async move { actor.run().await });
tx
});
pub static PRESENCE_DATA: Lazy<StdMutex<PresenceData>> =
Lazy::new(|| StdMutex::new(PresenceData::default()));
pub async fn connect() -> bool {
let (tx, rx) = oneshot::channel();
if PRESENCE_TX.send(PresenceCommand::Connect(tx)).await.is_ok() {
return rx.await.unwrap_or(false);
}
false
}
pub async fn disconnect() {
let (tx, rx) = oneshot::channel();
if PRESENCE_TX
.send(PresenceCommand::Disconnect(tx))
.await
.is_ok()
{
let _ = tokio::time::timeout(Duration::from_secs(2), rx).await;
}
}
pub async fn has_presence() -> bool {
let (tx, rx) = oneshot::channel();
if PRESENCE_TX
.send(PresenceCommand::IsConnected(tx))
.await
.is_ok()
{
return rx.await.unwrap_or(false);
}
false
}
pub fn update_status(state: Option<&str>, details: Option<&str>, large_image_key: Option<&str>) {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
if let Some(s) = state {
guard.state = s.to_string();
}
if let Some(d) = details {
guard.details = d.to_string();
}
if let Some(img) = large_image_key {
guard.large_image_key = img.to_string();
}
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn update_user(username: Option<&str>, id: Option<&str>) {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
guard.small_image_key = id.map(|s| format!("https://a.ez-pp.farm/{}", s));
guard.small_image_text = username.map(str::to_string);
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn set_button(label: &str, url: &str) {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
guard.dynamic_button = Some(PresenceButton {
label: label.to_string(),
url: url.to_string(),
});
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn clear_button() {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
guard.dynamic_button = None;
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn handle_run_event(_app: &AppHandle, event: &RunEvent) {
if let RunEvent::ExitRequested { .. } | RunEvent::Exit = event {
tauri::async_runtime::block_on(async {
disconnect().await;
});
}
}
-11
View File
@@ -1,11 +0,0 @@
// state.rs
use crate::config::Config;
use std::path::PathBuf;
use std::sync::Mutex;
pub struct AppState {
pub config: Mutex<Config>,
pub opened_files: Mutex<Vec<PathBuf>>,
/// Theme folder requested by the main window when it opens the editor.
pub editor_theme_folder: Mutex<Option<String>>,
}
-493
View File
@@ -1,493 +0,0 @@
//! Loopback HTTP server for theme assets on Linux and macOS.
//!
//! WebKitGTK cannot play audio or video through Tauri's custom `asset:`
//! protocol, and WKWebView's range handling makes large media stall there.
//! A real loopback HTTP server gives WebKit normal status codes, range
//! responses and MIME types, so v1 and v2 theme assets load the same way they
//! do in a browser without loading a whole video into a blob first.
use http_body_util::Full;
use http_range::HttpRange;
use hyper::body::{Bytes, Incoming};
use hyper::header::{
ACCEPT_RANGES, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS,
ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE,
CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ORIGIN, RANGE, VARY,
};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::convert::Infallible;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
use tokio::net::TcpListener;
type Body = Full<Bytes>;
/// Keep individual responses small enough that a media seek cannot allocate
/// an unbounded buffer. WebKit asks for the next range as it plays.
const MAX_RANGE_BYTES: u64 = 8 * 1024 * 1024;
struct ThemeServer {
port: u16,
root: PathBuf,
previews: Arc<Mutex<HashMap<String, PathBuf>>>,
}
static SERVER: OnceLock<ThemeServer> = OnceLock::new();
/// Bind the server on a random loopback port. The root is the themes folder.
pub fn start(root: PathBuf, media_root: PathBuf) -> Result<u16, String> {
if let Some(server) = SERVER.get() {
return Ok(server.port);
}
let root = std::fs::canonicalize(&root)
.map_err(|error| format!("Could not resolve the themes folder: {error}"))?;
let media_root = std::fs::canonicalize(&media_root)
.map_err(|error| format!("Could not resolve the theme media cache: {error}"))?;
let listener = std::net::TcpListener::bind(("127.0.0.1", 0))
.map_err(|error| format!("Could not start the theme asset server: {error}"))?;
listener
.set_nonblocking(true)
.map_err(|error| format!("Could not configure the theme asset server: {error}"))?;
let port = listener
.local_addr()
.map_err(|error| format!("Could not read the theme asset server address: {error}"))?
.port();
let previews = Arc::new(Mutex::new(HashMap::new()));
let server = ThemeServer {
port,
root: root.clone(),
previews: previews.clone(),
};
let _ = SERVER.set(server);
let root = Arc::new(root);
let media_root = Arc::new(media_root);
tauri::async_runtime::spawn(async move {
let listener = match TcpListener::from_std(listener) {
Ok(listener) => listener,
Err(error) => {
log::error!(target: "launcher", "Theme asset server failed: {error}");
return;
}
};
log::info!(
target: "launcher",
"Theme asset server listening on http://127.0.0.1:{port}/themes"
);
loop {
let (stream, _) = match listener.accept().await {
Ok(connection) => connection,
Err(error) => {
log::debug!(target: "launcher", "Theme asset server accept error: {error}");
continue;
}
};
let root = root.clone();
let media_root = media_root.clone();
let previews = previews.clone();
tokio::spawn(async move {
let service = service_fn(move |request| {
handle(request, root.clone(), media_root.clone(), previews.clone())
});
if let Err(error) = http1::Builder::new()
.serve_connection(TokioIo::new(stream), service)
.await
{
log::debug!(target: "launcher", "Theme asset connection ended: {error}");
}
});
}
});
Ok(port)
}
pub fn base_url() -> Option<String> {
SERVER
.get()
.map(|server| format!("http://127.0.0.1:{}/themes", server.port))
}
/// The cache folder for optimized theme media, next to the themes folder.
pub fn media_root() -> Result<PathBuf, String> {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| "Could not locate the home directory".to_string())?;
let root = PathBuf::from(home)
.join(".ezpplauncher")
.join("media-cache");
std::fs::create_dir_all(&root)
.map_err(|error| format!("Failed to create the theme media cache: {error}"))?;
std::fs::canonicalize(&root)
.map_err(|error| format!("Failed to resolve the theme media cache: {error}"))
}
/// Map a loopback theme URL back to the file it serves. Only paths inside the
/// configured themes folder are accepted.
pub fn resolve_theme_url(value: &str) -> Option<PathBuf> {
let server = SERVER.get()?;
let url = url::Url::parse(value).ok()?;
if url.scheme() != "http"
|| url.host_str() != Some("127.0.0.1")
|| url.port() != Some(server.port)
{
return None;
}
let path = percent_encoding::percent_decode_str(url.path())
.decode_utf8_lossy()
.to_string();
let relative = path.strip_prefix("/themes/")?;
safe_path(&server.root, relative)
}
/// Public URL for a file in the optimized media cache.
pub fn media_url(file_name: &str) -> Option<String> {
let server = SERVER.get()?;
Some(format!(
"http://127.0.0.1:{}/media/{}",
server.port, file_name
))
}
/// Register a file picked in the theme editor so its preview can be served
/// over HTTP too. The token is only a lookup key; unregistered paths are never
/// readable through the server.
pub fn preview_url(path: &Path) -> Result<String, String> {
let server = SERVER
.get()
.ok_or_else(|| "The theme asset server is not running".to_string())?;
let path = std::fs::canonicalize(path)
.map_err(|error| format!("Could not resolve the preview asset: {error}"))?;
if !path.is_file() {
return Err("The preview asset is not a file".to_string());
}
let mut hasher = Sha256::new();
hasher.update(path.to_string_lossy().as_bytes());
hasher.update(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_le_bytes(),
);
let token = hex::encode(&hasher.finalize()[..16]);
server
.previews
.lock()
.map_err(|_| "Could not register the preview asset".to_string())?
.insert(token.clone(), path);
Ok(format!("http://127.0.0.1:{}/preview/{token}", server.port))
}
async fn handle(
request: Request<Incoming>,
root: Arc<PathBuf>,
media_root: Arc<PathBuf>,
previews: Arc<Mutex<HashMap<String, PathBuf>>>,
) -> Result<Response<Body>, Infallible> {
let origin = request
.headers()
.get(ORIGIN)
.and_then(|value| value.to_str().ok())
.and_then(allowed_origin);
if request.method() == Method::OPTIONS {
return Ok(preflight_response(origin.as_deref()));
}
if request.method() != Method::GET && request.method() != Method::HEAD {
return Ok(text_response(
StatusCode::METHOD_NOT_ALLOWED,
"Method not allowed",
origin.as_deref(),
));
}
let range = request
.headers()
.get(RANGE)
.and_then(|value| value.to_str().ok())
.map(ToOwned::to_owned);
let path = percent_encoding::percent_decode_str(request.uri().path())
.decode_utf8_lossy()
.to_string();
log::debug!(
target: "launcher",
"Theme asset request: {path} range={range:?}"
);
let file = if let Some(relative) = path.strip_prefix("/themes/") {
safe_path(&root, relative)
} else if let Some(relative) = path.strip_prefix("/media/") {
safe_path(&media_root, relative)
} else if let Some(token) = path.strip_prefix("/preview/") {
previews
.lock()
.ok()
.and_then(|previews| previews.get(token).cloned())
} else {
None
};
let response = match file {
Some(file) => {
serve_file(request.method(), &file, range.as_deref(), origin.as_deref()).await
}
None => Ok(text_response(
StatusCode::NOT_FOUND,
"Not found",
origin.as_deref(),
)),
};
Ok(response.unwrap_or_else(|message| {
log::warn!(target: "launcher", "Theme asset request failed: {message}");
text_response(
StatusCode::INTERNAL_SERVER_ERROR,
&message,
origin.as_deref(),
)
}))
}
fn safe_path(root: &Path, relative: &str) -> Option<PathBuf> {
let relative = relative.trim_start_matches('/');
if relative.is_empty()
|| relative
.split('/')
.any(|part| part.is_empty() || part == "." || part == "..")
{
return None;
}
let candidate = root.join(relative);
let canonical = std::fs::canonicalize(candidate).ok()?;
canonical.starts_with(root).then_some(canonical)
}
async fn serve_file(
method: &Method,
path: &Path,
range_header: Option<&str>,
origin: Option<&str>,
) -> Result<Response<Body>, String> {
let metadata = tokio::fs::metadata(path)
.await
.map_err(|error| format!("Could not read theme asset: {error}"))?;
if !metadata.is_file() {
return Ok(text_response(StatusCode::NOT_FOUND, "Not found", origin));
}
let length = metadata.len();
let range = match range_header {
Some(header) => match parse_range(header, length) {
Ok(range) => range,
Err(()) => return Ok(range_not_satisfiable(length, origin)),
},
None => None,
};
let (status, start, end) = match range {
Some((start, end)) => (StatusCode::PARTIAL_CONTENT, start, end),
None => (StatusCode::OK, 0, length.saturating_sub(1)),
};
let body_length = if length == 0 {
0
} else {
end.saturating_sub(start) + 1
};
let body = if method == Method::HEAD || body_length == 0 {
Vec::new()
} else {
let mut file = tokio::fs::File::open(path)
.await
.map_err(|error| format!("Could not open theme asset: {error}"))?;
if start > 0 {
file.seek(SeekFrom::Start(start))
.await
.map_err(|error| format!("Could not seek theme asset: {error}"))?;
}
let mut buffer = vec![0_u8; body_length as usize];
file.read_exact(&mut buffer)
.await
.map_err(|error| format!("Could not read theme asset: {error}"))?;
buffer
};
let mut builder = Response::builder()
.status(status)
.header(CONTENT_TYPE, content_type(path))
.header(CONTENT_LENGTH, body_length.to_string())
.header(ACCEPT_RANGES, "bytes")
.header("Cache-Control", "no-store")
.header("X-Content-Type-Options", "nosniff")
.header("Cross-Origin-Resource-Policy", "cross-origin");
if status == StatusCode::PARTIAL_CONTENT {
builder = builder.header(CONTENT_RANGE, format!("bytes {start}-{end}/{length}"));
}
if let Some(origin) = origin {
builder = builder
.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin)
.header(VARY, "Origin")
.header(
ACCESS_CONTROL_EXPOSE_HEADERS,
"Content-Range, Content-Length, Accept-Ranges",
);
}
builder
.body(Full::new(body.into()))
.map_err(|error| format!("Could not build theme asset response: {error}"))
}
fn parse_range(header: &str, length: u64) -> Result<Option<(u64, u64)>, ()> {
if length == 0 {
return Ok(None);
}
let ranges = HttpRange::parse(header, length).map_err(|_| ())?;
if ranges.len() != 1 {
return Err(());
}
let range = &ranges[0];
let start = range.start;
let end = (start + range.length - 1).min(length - 1);
if start >= length || end < start {
return Err(());
}
// Don't allocate the entire file for a single open-ended range request.
Ok(Some((start, end.min(start + MAX_RANGE_BYTES - 1))))
}
fn range_not_satisfiable(length: u64, origin: Option<&str>) -> Response<Body> {
let mut builder = Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{length}"))
.header(CONTENT_TYPE, "text/plain; charset=utf-8")
.header(CONTENT_LENGTH, "0");
if let Some(origin) = origin {
builder = builder
.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin)
.header(VARY, "Origin");
}
builder
.body(Full::new(Bytes::new()))
.expect("range response is valid")
}
fn preflight_response(origin: Option<&str>) -> Response<Body> {
let mut builder = Response::builder()
.status(StatusCode::NO_CONTENT)
.header(ACCESS_CONTROL_ALLOW_METHODS, "GET, HEAD, OPTIONS")
.header(ACCESS_CONTROL_ALLOW_HEADERS, "Range, Content-Type")
.header(
ACCESS_CONTROL_EXPOSE_HEADERS,
"Content-Range, Content-Length, Accept-Ranges",
)
.header(ACCESS_CONTROL_MAX_AGE, "600");
if let Some(origin) = origin {
builder = builder
.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin)
.header(VARY, "Origin");
}
builder
.body(Full::new(Bytes::new()))
.expect("preflight response is valid")
}
fn text_response(status: StatusCode, message: &str, origin: Option<&str>) -> Response<Body> {
let mut builder = Response::builder()
.status(status)
.header(CONTENT_TYPE, "text/plain; charset=utf-8")
.header(CONTENT_LENGTH, message.len().to_string())
.header("X-Content-Type-Options", "nosniff");
if let Some(origin) = origin {
builder = builder
.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin)
.header(VARY, "Origin");
}
builder
.body(Full::new(Bytes::copy_from_slice(message.as_bytes())))
.expect("text response is valid")
}
fn allowed_origin(origin: &str) -> Option<String> {
if origin == "tauri://localhost"
|| origin == "http://tauri.localhost"
|| origin == "https://tauri.localhost"
|| origin == "null"
|| origin.starts_with("http://localhost:")
|| origin.starts_with("http://127.0.0.1:")
{
Some(origin.to_string())
} else {
None
}
}
fn content_type(path: &Path) -> &'static str {
match path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default()
.to_ascii_lowercase()
.as_str()
{
"html" | "htm" => "text/html; charset=utf-8",
"js" | "mjs" => "text/javascript; charset=utf-8",
"css" => "text/css; charset=utf-8",
"json" => "application/json; charset=utf-8",
"svg" => "image/svg+xml",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"webp" => "image/webp",
"avif" => "image/avif",
"ico" => "image/x-icon",
"webm" => "video/webm",
"mp4" | "m4v" => "video/mp4",
"mov" => "video/quicktime",
"ogv" => "video/ogg",
"mp3" => "audio/mpeg",
"ogg" | "oga" => "audio/ogg",
"wav" => "audio/wav",
"m4a" => "audio/mp4",
"flac" => "audio/flac",
"woff2" => "font/woff2",
"woff" => "font/woff",
"ttf" => "font/ttf",
"otf" => "font/otf",
"wasm" => "application/wasm",
_ => "application/octet-stream",
}
}
-515
View File
@@ -1,515 +0,0 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use sysinfo::Pid;
pub const OSU_FOLDER_FILES: &[&str] = &[
"avcodec-51.dll",
"avformat-52.dll",
"avutil-49.dll",
"bass.dll",
"bass_fx.dll",
"collection.db",
"d3dcompiler_47.dll",
"libEGL.dll",
"libGLESv2.dll",
"Microsoft.Ink.dll",
"OpenTK.dll",
"osu!.cfg",
"osu!.db",
"osu!.exe",
"osu!auth.dll",
"osu!gameplay.dll",
"osu!seasonal.dll",
"osu!ui.dll",
"presence.db",
"pthreadGC2.dll",
"scores.db",
];
pub fn is_valid_osu_folder<P: AsRef<Path>>(folder_path: P) -> bool {
let folder_path = folder_path.as_ref();
folder_path.join("osu!.exe").is_file()
&& check_folder_completeness(folder_path, OSU_FOLDER_FILES) >= 70.0
}
/// True if the process is osu! (either running natively or through Wine/Proton).
pub fn is_osu_process(process: &sysinfo::Process) -> bool {
let name = process.name().to_string_lossy().to_ascii_lowercase();
if name.ends_with("osu!.exe") {
return true;
}
if let Some(exe) = process.exe() {
if exe
.file_name()
.map(|file_name| file_name.to_string_lossy().to_ascii_lowercase() == "osu!.exe")
.unwrap_or(false)
{
return true;
}
}
process.cmd().iter().any(|arg| {
arg.to_string_lossy()
.to_ascii_lowercase()
.contains("osu!.exe")
})
}
/// Like [`is_osu_process`], but excludes the osu! updater started with `-repair`.
pub fn is_osu_game_process(process: &sysinfo::Process) -> bool {
is_osu_process(process)
&& !process.cmd().iter().any(|arg| {
arg.to_string_lossy()
.to_ascii_lowercase()
.contains("-repair")
})
}
fn find_osu_user_config_path<P: AsRef<Path>>(folder_path: P) -> Option<PathBuf> {
let folder_path = folder_path.as_ref();
if !folder_path.is_dir() {
return None;
}
if let Ok(current_user) = std::env::var("USERNAME").or_else(|_| std::env::var("USER")) {
let config_path = folder_path.join(format!("osu!.{}.cfg", current_user));
if config_path.is_file() {
return Some(config_path);
}
}
// The Windows username inside a Wine prefix does not always match the host
// username (especially on macOS), so fall back to the most recent osu!.*.cfg.
let mut candidates = fs::read_dir(folder_path)
.ok()?
.flatten()
.filter_map(|entry| {
let path = entry.path();
let file_name = path.file_name()?.to_string_lossy().to_ascii_lowercase();
if !file_name.starts_with("osu!.") || !file_name.ends_with(".cfg") {
return None;
}
if file_name == "osu!.cfg" {
return None;
}
let modified = entry
.metadata()
.and_then(|metadata| metadata.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
Some((modified, path))
})
.collect::<Vec<_>>();
candidates.sort_by(|a, b| b.0.cmp(&a.0));
candidates.into_iter().next().map(|(_, path)| path)
}
pub fn check_folder_completeness<P: AsRef<Path>>(folder_path: P, required_files: &[&str]) -> f32 {
let mut found = 0;
for file in required_files {
let file_path = folder_path.as_ref().join(file);
if file_path.exists() {
found += 1;
}
}
if required_files.is_empty() {
100.0
} else {
(found as f32 / required_files.len() as f32) * 100.0
}
}
pub fn get_osu_user_config<P: AsRef<Path>>(
osu_folder_path: P,
) -> Option<std::collections::HashMap<String, String>> {
let osu_config_path = find_osu_user_config_path(osu_folder_path)?;
let mut config_map = std::collections::HashMap::new();
if let Ok(contents) = std::fs::read_to_string(osu_config_path) {
for line in contents.lines() {
if let Some((key, value)) = line.split_once(" = ") {
config_map.insert(key.trim().to_string(), value.trim().to_string());
}
}
}
Some(config_map)
}
pub fn set_osu_user_config_vals(
osu_folder_path: &str,
key_values: &[(&str, Option<&str>)],
) -> Result<bool, String> {
let osu_config_path = find_osu_user_config_path(osu_folder_path)
.ok_or_else(|| "osu! user config file does not exist".to_string())?;
let mut lines = fs::read_to_string(&osu_config_path)
.map_err(|e| e.to_string())?
.lines()
.map(|line| line.to_string())
.collect::<Vec<String>>();
let mut keys_to_set: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
let mut keys_to_add: std::collections::HashSet<&str> = std::collections::HashSet::new();
for (key, value_opt) in key_values.iter() {
if let Some(value) = value_opt {
keys_to_set.insert(*key, *value);
keys_to_add.insert(*key);
}
}
let mut updates = Vec::new();
for (i, line) in lines.iter().enumerate() {
if let Some((existing_key, _)) = line.split_once(" = ") {
let trimmed_key = existing_key.trim();
if let Some(new_value) = keys_to_set.get(trimmed_key) {
updates.push((i, trimmed_key.to_string(), new_value.to_string()));
}
}
}
for (i, trimmed_key, new_value) in updates {
lines[i] = format!("{} = {}", trimmed_key, new_value);
keys_to_add.remove(trimmed_key.as_str());
}
for key in keys_to_add {
if let Some(value) = keys_to_set.get(key) {
lines.push(format!("{} = {}", key, value));
}
}
let new_content = lines.join("\n") + "\n";
fs::write(&osu_config_path, new_content).map_err(|e| e.to_string())?;
Ok(true)
}
pub fn set_osu_config_vals(
osu_folder_path: &str,
key_values: &[(&str, Option<&str>)],
) -> Result<bool, String> {
let osu_config_path = Path::new(osu_folder_path).join("osu!.cfg");
if !osu_config_path.exists() {
return Err("osu!.cfg file does not exist".to_string());
}
let mut lines = fs::read_to_string(&osu_config_path)
.map_err(|e| e.to_string())?
.lines()
.map(|line| line.to_string())
.collect::<Vec<String>>();
let mut keys_to_set: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
let mut keys_to_add: std::collections::HashSet<&str> = std::collections::HashSet::new();
for (key, value_opt) in key_values.iter() {
if let Some(value) = value_opt {
keys_to_set.insert(*key, *value);
keys_to_add.insert(*key);
}
}
let mut updates = Vec::new();
for (i, line) in lines.iter().enumerate() {
if let Some((existing_key, _)) = line.split_once(" = ") {
let trimmed_key = existing_key.trim();
if let Some(new_value) = keys_to_set.get(trimmed_key) {
updates.push((i, trimmed_key.to_string(), new_value.to_string()));
}
}
}
for (i, trimmed_key, new_value) in updates {
lines[i] = format!("{} = {}", trimmed_key, new_value);
keys_to_add.remove(trimmed_key.as_str());
}
for key in keys_to_add {
if let Some(value) = keys_to_set.get(key) {
lines.push(format!("{} = {}", key, value));
}
}
let new_content = lines.join("\n") + "\n";
fs::write(&osu_config_path, new_content).map_err(|e| e.to_string())?;
Ok(true)
}
pub fn get_osu_config<P: AsRef<Path>>(
osu_folder_path: P,
) -> Option<std::collections::HashMap<String, String>> {
if !osu_folder_path.as_ref().exists() {
return None;
}
let osu_config_path = osu_folder_path.as_ref().join("osu!.cfg");
if !osu_config_path.exists() {
return None;
}
let mut config_map = std::collections::HashMap::new();
if let Ok(contents) = std::fs::read_to_string(osu_config_path) {
for line in contents.lines() {
if let Some((key, value)) = line.split_once(" = ") {
config_map.insert(key.trim().to_string(), value.trim().to_string());
}
}
}
return Some(config_map);
}
#[cfg(target_os = "linux")]
pub fn get_osuwinello_path() -> Option<PathBuf> {
let mut search_paths =
std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()).collect::<Vec<_>>();
for path in ["/usr/local/bin", "/usr/bin", "/bin"] {
let path = PathBuf::from(path);
if !search_paths.contains(&path) {
search_paths.push(path);
}
}
if let Some(home) = std::env::var_os("HOME") {
let path = Path::new(&home).join(".local/bin");
if !search_paths.contains(&path) {
search_paths.push(path);
}
}
search_paths
.into_iter()
.map(|path| path.join("osu-wine"))
.find(|path| path.is_file())
}
#[cfg(not(target_os = "linux"))]
pub fn get_osuwinello_path() -> Option<PathBuf> {
None
}
#[cfg(target_os = "linux")]
pub fn is_osuwinello_available() -> bool {
get_osuwinello_path().is_some()
}
#[cfg(not(target_os = "linux"))]
pub fn is_osuwinello_available() -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn is_wmctrl_available() -> bool {
use std::process::Command;
Command::new("wmctrl")
.arg("-V")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
#[cfg(not(target_os = "linux"))]
pub fn is_wmctrl_available() -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn get_window_title_by_pid(target_pid: Pid) -> String {
use std::process::Command;
let find_title = || -> Option<String> {
let output = Command::new("wmctrl").arg("-lp").output().ok()?;
if !output.status.success() {
return None;
}
let output_str = String::from_utf8(output.stdout).ok()?;
for line in output_str.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
continue;
}
if let Ok(pid) = parts[2].parse::<u32>() {
if pid == target_pid.as_u32() {
let title = parts[4..].join(" ");
return Some(title);
}
}
}
None
};
find_title().unwrap_or_default()
}
#[cfg(target_os = "macos")]
pub fn get_window_title_by_pid(_pid: Pid) -> String {
String::new()
}
#[cfg(windows)]
pub fn get_window_title_by_pid(pid: Pid) -> String {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::sync::{Arc, Mutex};
use winapi::shared::windef::HWND;
use winapi::um::winuser::{
EnumWindows, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible,
};
extern "system" fn enum_windows_proc(
hwnd: HWND,
lparam: winapi::shared::minwindef::LPARAM,
) -> i32 {
unsafe {
let data = &mut *(lparam as *mut (u32, Arc<Mutex<Option<String>>>));
let target_pid = data.0;
let result = &data.1;
let mut window_pid = 0u32;
GetWindowThreadProcessId(hwnd, &mut window_pid);
if window_pid == target_pid && IsWindowVisible(hwnd) != 0 {
let mut title = vec![0u16; 512];
let length = GetWindowTextW(hwnd, title.as_mut_ptr(), title.len() as i32);
if length > 0 {
let title = OsString::from_wide(&title[..length as usize]);
let title_str = title.to_string_lossy().into_owned();
if !title_str.is_empty() {
*result.lock().unwrap() = Some(title_str);
return 0;
}
}
}
1
}
}
let result = Arc::new(Mutex::new(None));
let mut data = (pid.as_u32(), Arc::clone(&result));
unsafe {
EnumWindows(
Some(enum_windows_proc),
&mut data as *mut _ as winapi::shared::minwindef::LPARAM,
);
}
result.lock().unwrap().clone().unwrap_or_default()
}
pub async fn is_net8_installed() -> bool {
use tokio::process::Command;
let mut command = Command::new("dotnet");
command.arg("--list-runtimes");
#[cfg(windows)]
{
const CREATE_NO_WINDOW: u32 = 0x08000000;
command.creation_flags(CREATE_NO_WINDOW);
}
let output_result = command.output().await;
match output_result {
Ok(output) => {
if !output.status.success() {
log::warn!(
target: "launcher",
"`dotnet --list-runtimes` failed with status: {}",
output.status
);
log::warn!(
target: "launcher",
"dotnet stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
return false;
}
let stdout_str = String::from_utf8_lossy(&output.stdout);
stdout_str
.lines()
.any(|line| line.starts_with("Microsoft.WindowsDesktop.App 8."))
}
Err(_) => false,
}
}
#[cfg(not(windows))]
pub fn encrypt_password(password: &str, _entropy: &str) -> Result<String, String> {
Ok(password.to_string())
}
#[cfg(windows)]
pub fn encrypt_password(password: &str, entropy: &str) -> Result<String, String> {
use base64::{Engine as _, engine::general_purpose};
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::ptr;
use std::slice;
use winapi::shared::minwindef::{BYTE, DWORD, LPVOID};
use winapi::shared::ntdef::LPCWSTR;
use winapi::um::dpapi::{CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData};
use winapi::um::winbase::LocalFree;
use winapi::um::wincrypt::DATA_BLOB;
let description = "Encrypted";
let password_bytes = password.as_bytes();
let mut input_blob = DATA_BLOB {
cbData: password_bytes.len() as DWORD,
pbData: password_bytes.as_ptr() as *mut BYTE,
};
let entropy_bytes = entropy.as_bytes();
let mut entropy_blob = DATA_BLOB {
cbData: entropy_bytes.len() as DWORD,
pbData: entropy_bytes.as_ptr() as *mut BYTE,
};
let mut output_blob = DATA_BLOB {
cbData: 0,
pbData: ptr::null_mut(),
};
let wide_description: Vec<u16> = OsStrExt::encode_wide(OsStr::new(description))
.chain(Some(0))
.collect();
let p_description: LPCWSTR = wide_description.as_ptr();
let result = unsafe {
CryptProtectData(
&mut input_blob,
p_description,
&mut entropy_blob,
ptr::null_mut(),
ptr::null_mut(),
CRYPTPROTECT_UI_FORBIDDEN,
&mut output_blob,
)
};
if result == 0 {
return Err("CryptProtectData failed".to_string());
}
let encrypted_data =
unsafe { slice::from_raw_parts(output_blob.pbData, output_blob.cbData as usize).to_vec() };
unsafe {
LocalFree(output_blob.pbData as LPVOID);
}
let base64_string = general_purpose::STANDARD.encode(&encrypted_data);
Ok(base64_string)
}
-99
View File
@@ -1,99 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "EZPPLauncher",
"version": "3.2.7",
"identifier": "farm.EZPPFarm.Launcher",
"mainBinaryName": "ezpplauncher",
"build": {
"beforeDevCommand": "bun run vite:dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "EZPPLauncher",
"width": 1000,
"height": 650,
"minWidth": 1000,
"minHeight": 650,
"decorations": false,
"resizable": false,
"maximizable": false,
"shadow": true
}
],
"withGlobalTauri": true,
"security": {
"csp": {
"default-src": "'self' tauri: customprotocol: asset:",
"script-src": "'self' 'unsafe-inline' asset: http://asset.localhost https://asset.localhost http://127.0.0.1:* https://analytics.ez-pp.farm",
"style-src": "'self' 'unsafe-inline' asset: http://asset.localhost https://asset.localhost http://127.0.0.1:*",
"img-src": "'self' asset: http://asset.localhost https://asset.localhost blob: data: http://127.0.0.1:* https://assets.ppy.sh https://a.ez-pp.farm https://git.ez-pp.farm https://osu.direct",
"connect-src": "'self' ipc: http://ipc.localhost asset: http://asset.localhost https://asset.localhost http://127.0.0.1:* https://ez-pp.farm https://api.ez-pp.farm https://c.ez-pp.farm https://analytics.ez-pp.farm https://git.ez-pp.farm https://osu.direct https://osu.ppy.sh https://api.fontsource.org https://cdn.jsdelivr.net",
"media-src": "'self' asset: http://asset.localhost https://asset.localhost http://127.0.0.1:* blob:",
"font-src": [
"'self'",
"asset:",
"blob:",
"data:",
"http://asset.localhost",
"https://asset.localhost",
"http://127.0.0.1:*",
"https://cdn.jsdelivr.net"
]
},
"assetProtocol": {
"enable": true,
"scope": [
"$HOME/.ezpplauncher/themes/**",
"$HOME/.ezpplauncher/font-cache/**"
]
}
}
},
"bundle": {
"publisher": "EZPPFarm",
"copyright": "EZPPFarm",
"shortDescription": "osu! launcher for EZPPFarm",
"longDescription": "EZPPLauncher is a custom osu! launcher for the EZPPFarm server, featuring automatic updates, ingame login, stat tracking and launcher theming.",
"homepage": "https://ez-pp.farm",
"license": "GPL-3.0",
"active": true,
"targets": [
"nsis",
"appimage"
],
"fileAssociations": [
{
"ext": [
"ezpplauncher-theme"
],
"name": "EZPPLauncher Theme",
"description": "EZPPLauncher Theme File",
"role": "Viewer",
"mimeType": "application/x-ezpplauncher-theme"
}
],
"windows": {
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"
},
"nsis": {
"installMode": "currentUser",
"compression": "lzma",
"installerIcon": "icons/icon.ico",
"installerHooks": "./nsis/nsis-hooks.nsh"
}
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"bundle": {
"targets": ["app", "dmg"],
"macOS": {
"minimumSystemVersion": "10.15"
}
}
}
-172
View File
@@ -1,172 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:where(.dark, .dark *));
@import "@fontsource-variable/sora";
@import "@fontsource/space-mono";
@import "@fontsource/bebas-neue";
@import "@fontsource-variable/montserrat";
@import "@fontsource-variable/playfair-display";
@import "@fontsource-variable/jetbrains-mono";
:root {
--radius: 0.625rem;
--background: oklch(95.36% 0.017 256.57);
--foreground: oklch(40.06% 0.038 266.38);
--card: oklch(100% 0 0);
--card-foreground: oklch(40.06% 0.038 266.38);
--popover: oklch(84.01% 0.028 258.47);
--popover-foreground: oklch(40.06% 0.038 266.38);
--primary: oklch(60.7% 0.226 299.68);
--primary-foreground: oklch(100% 0 0);
--secondary: oklch(84.01% 0.028 258.47);
--secondary-foreground: oklch(40.06% 0.038 266.38);
--muted: oklch(89.49% 0.023 256.59);
--muted-foreground: oklch(51.01% 0.028 265.36);
--accent: oklch(54.41% 0.186 230.5);
--accent-foreground: oklch(100% 0 0);
--destructive: oklch(51.68% 0.202 15.84);
--destructive-foreground: oklch(100% 0 0);
--border: oklch(78.52% 0.026 260.1);
--input: oklch(84.01% 0.028 258.47);
--ring: oklch(60.7% 0.226 299.68);
--chart-1: oklch(60.7% 0.226 299.68);
--chart-2: oklch(54.41% 0.186 230.5);
--chart-3: oklch(46.49% 0.139 139.66);
--chart-4: oklch(60.01% 0.202 55.86);
--chart-5: oklch(70.11% 0.125 39.15);
--sidebar: oklch(92.65% 0.019 256.57);
--sidebar-background: oklch(92.65% 0.019 256.57);
--sidebar-foreground: oklch(40.06% 0.038 266.38);
--sidebar-primary: oklch(60.7% 0.226 299.68);
--sidebar-primary-foreground: oklch(100% 0 0);
--sidebar-accent: oklch(54.41% 0.186 230.5);
--sidebar-accent-foreground: oklch(100% 0 0);
--sidebar-border: oklch(78.52% 0.026 260.1);
--sidebar-ring: oklch(60.7% 0.226 299.68);
--font-sans:
"Sora Variable", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-mono:
"Space Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
}
.dark {
--background: oklch(17.02% 0.042 282.86);
--foreground: oklch(89.24% 0.079 263.24);
--card: oklch(20.22% 0.048 282.86);
--card-foreground: oklch(89.24% 0.079 263.24);
--popover: oklch(20.3% 0.05 282.86);
--popover-foreground: oklch(89.24% 0.079 263.24);
--primary: oklch(72.08% 0.196 356.34);
--primary-foreground: oklch(20.22% 0.048 282.86);
--secondary: oklch(42.99% 0.031 270.49);
--secondary-foreground: oklch(89.24% 0.079 263.24);
--muted: oklch(24.41% 0.048 271.21);
--muted-foreground: oklch(73.83% 0.057 265.65);
--accent: oklch(86.2% 0.131 300.42);
--accent-foreground: oklch(20.22% 0.048 282.86);
--destructive: oklch(77.23% 0.146 11.66);
--destructive-foreground: oklch(20.22% 0.048 282.86);
--border: oklch(27.7% 0.041 276.87);
--input: oklch(27.7% 0.041 276.87);
--ring: oklch(72.08% 0.196 356.34);
--chart-1: oklch(72.08% 0.196 356.34);
--chart-2: oklch(77.19% 0.121 223.01);
--chart-3: oklch(79.1% 0.119 150.21);
--chart-4: oklch(79.74% 0.167 57.97);
--chart-5: oklch(92.11% 0.061 37.8);
--sidebar: oklch(13.29% 0.036 282.86);
--sidebar-background: oklch(13.29% 0.036 282.86);
--sidebar-foreground: oklch(89.24% 0.079 263.24);
--sidebar-primary: oklch(72.08% 0.196 356.34);
--sidebar-primary-foreground: oklch(20.22% 0.048 282.86);
--sidebar-accent: oklch(75.21% 0.121 293.01);
--sidebar-accent-foreground: oklch(20.22% 0.048 282.86);
--sidebar-border: oklch(35.13% 0.03 273.31);
--sidebar-ring: oklch(72.08% 0.196 356.34);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-theme: #181825;
--color-theme-50: #e9e9f1;
--color-theme-100: #dbdbe8;
--color-theme-200: #bfbfd6;
--color-theme-300: #a4a4c4;
--color-theme-400: #8888b2;
--color-theme-500: #6c6ca0;
--color-theme-600: #575786;
--color-theme-700: #45456b;
--color-theme-800: #33334f;
--color-theme-900: #212133;
--color-theme-950: #181825;
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-50: #ffe5f1;
--color-primary-100: #ffcce2;
--color-primary-200: #ff99c5;
--color-primary-300: #ff66a8;
--color-primary-400: #ff338b;
--color-primary-500: #ff006e;
--color-primary-600: #cc0058;
--color-primary-700: #990042;
--color-primary-800: #66002c;
--color-primary-900: #330016;
--color-primary-950: #24000f;
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
body {
@apply bg-background text-foreground;
}
button:not([disabled]),
[role="button"]:not([disabled]) {
cursor: pointer;
}
}
-13
View File
@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EZPPLauncher</title>
%sveltekit.head%
</head>
<body class="bg-theme-950" data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

-121
View File
@@ -1,121 +0,0 @@
import type {
EZPPUser,
EZPPUserInfoResponse,
EZPPUserResponse,
EZPPUSerStatusResponse,
StreamsResult,
} from '$lib/types';
import { betterFetch } from '@better-fetch/fetch';
const VERSION_ENDPOINT = 'https://version.ez-pp.farm/';
const BANCHO_ENDPOINT = 'https://c.ez-pp.farm/';
const API_ENDPOINT = 'https://api.ez-pp.farm/';
const ENDPOINT = 'https://ez-pp.farm/';
const timeout = 5000; // 5 seconds;
export const ezppfarm = {
ping: async (): Promise<number | undefined> => {
try {
const start = Date.now();
const request = await betterFetch(BANCHO_ENDPOINT, {
timeout,
headers: {
'User-Agent': 'EZPPLauncher',
},
});
if (request.error) return undefined;
const ping = Date.now() - start;
return ping;
} catch {
return undefined;
}
},
login: async (
username: string,
password: string
): Promise<
| {
code: number;
message: string;
user?: EZPPUser;
}
| undefined
> => {
const request = await betterFetch<EZPPUserResponse>(`${ENDPOINT}login/check`, {
method: 'POST',
timeout,
body: JSON.stringify({
username: username,
password: password,
}),
headers: {
'Content-Type': 'application/json',
'User-Agent': 'EZPPLauncher',
},
});
if (request.error) {
if (request.error.status >= 500 && request.error.status < 600)
throw new Error('Server not reachable');
return undefined;
}
return request.data;
},
getUserInfo: async (userId: number, scope: 'all' | 'info' | 'stats' = 'all') => {
const request = await betterFetch<EZPPUserInfoResponse>(`${API_ENDPOINT}v1/get_player_info`, {
timeout,
query: {
id: userId,
scope,
},
headers: {
'Content-Type': 'application/json',
'User-Agent': 'EZPPLauncher',
},
});
return request.error ? undefined : request.data;
},
getUserStatus: async (userId: number) => {
const request = await betterFetch<EZPPUSerStatusResponse>(
`${API_ENDPOINT}v1/get_player_status`,
{
timeout,
query: {
id: userId,
},
headers: {
'Content-Type': 'application/json',
'User-Agent': 'EZPPLauncher',
},
}
);
return request.error ? undefined : request.data;
},
latestBuildVersion: async (releaseStream: string): Promise<string | undefined> => {
const request = await betterFetch<StreamsResult>(`${VERSION_ENDPOINT}api/changelog`, {
timeout,
query: {
stream: 'none',
},
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'EZPPLauncher',
},
});
if (request.error) {
return undefined;
}
const releaseData = request.data;
if (!releaseData || !releaseData.streams) {
return undefined;
}
if (releaseData.streams.length === 0) return undefined;
const selectedRelease = releaseData.streams.find(
(releaseBuild) =>
releaseBuild.name.toLowerCase() === releaseStream.replaceAll(' ', '').toLowerCase()
);
if (!selectedRelease) return undefined;
return selectedRelease.latest_build.display_version;
},
};
-33
View File
@@ -1,33 +0,0 @@
import type { Release } from '$lib/types';
import { betterFetch } from '@better-fetch/fetch';
import semver from 'semver';
const ENDPOINT = 'https://git.ez-pp.farm/';
const timeout = 5000; // 5 seconds;
export const git = {
hasUpdate: async (version: string): Promise<Release | undefined> => {
try {
const request = await betterFetch<Release[]>(
`${ENDPOINT}api/v1/repos/EZPPFarm/EZPPLauncher/releases?limit=1`,
{
timeout,
headers: {
'User-Agent': 'EZPPLauncher',
},
}
);
if (request.error) return undefined;
const latestRelease = request.data[0];
if (!latestRelease) return undefined;
const hasUpdate = semver.lt(version, latestRelease.tag_name);
return hasUpdate ? latestRelease : undefined;
} catch {
return undefined;
}
},
};
-11
View File
@@ -1,11 +0,0 @@
const API_ENDPOINT = 'https://osu.direct/api/';
export const osudirect = {
osu: async (mapId: number): Promise<string | undefined> => {
try {
return await (await fetch(API_ENDPOINT + `osu/${mapId}?raw`)).text();
} catch {
return undefined;
}
},
};
@@ -1,182 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
type ConfettiCannonProps = {
colors?: string[];
count?: number;
};
let { colors = ['#ff006e', '#a78bfa', '#7dd3fc', '#fbbf24'], count = 820 }: ConfettiCannonProps =
$props();
let canvas = $state<HTMLCanvasElement>();
type Particle = {
x: number;
y: number;
vx: number;
vy: number;
width: number;
height: number;
rotation: number;
spin: number;
flutterPhase: number;
flutterSpeed: number;
swayPhase: number;
swaySpeed: number;
swayAmount: number;
terminalVelocity: number;
wind: number;
color: string;
delay: number;
age: number;
lifetime: number;
};
onMount(() => {
const element = canvas;
if (!element) return;
const context = element.getContext('2d');
if (!context) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const random = (min: number, max: number) => Math.random() * (max - min) + min;
let width = 0;
let height = 0;
let dpr = 1;
let frame = 0;
let lastTime = 0;
let elapsed = 0;
let particles: Particle[] = [];
const spawn = () => {
const rect = element.getBoundingClientRect();
width = rect.width || window.innerWidth;
height = rect.height || window.innerHeight;
dpr = Math.min(window.devicePixelRatio || 1, 2);
element.width = Math.max(1, Math.round(width * dpr));
element.height = Math.max(1, Math.round(height * dpr));
context.setTransform(dpr, 0, 0, dpr, 0, 0);
particles = Array.from({ length: count }, (_, index) => {
const fromLeft = index % 2 === 0;
const direction = fromLeft ? 1 : -1;
const streamer = Math.random() < 0.35;
const pieceWidth = streamer ? random(3, 5) : random(5, 8);
const pieceHeight = streamer ? random(12, 24) : random(7, 14);
return {
x: fromLeft ? random(0, 28) : width - random(0, 28),
y: height - random(0, 16),
// Aim inward and up from each bottom corner. Horizontal drag and
// gravity turn the straight launch into a natural arc.
vx: direction * random(160, 900),
vy: -random(1050, 1450),
width: pieceWidth,
height: pieceHeight,
rotation: random(0, Math.PI * 2),
spin: random(-4, 4),
flutterPhase: random(0, Math.PI * 2),
flutterSpeed: random(2.5, 6.5),
swayPhase: random(0, Math.PI * 2),
swaySpeed: random(2.2, 4.2),
swayAmount: random(4, 14),
terminalVelocity: random(130, 230),
wind: random(-8, 8),
color: colors[Math.floor(Math.random() * colors.length)],
delay: random(0, 0.25),
age: 0,
lifetime: random(7, 9.5),
};
});
};
const paint = (time: number) => {
if (lastTime === 0) lastTime = time;
const delta = Math.min((time - lastTime) / 1000, 0.05);
lastTime = time;
elapsed += delta;
context.clearRect(0, 0, width, height);
let alive = false;
for (const particle of particles) {
if (elapsed < particle.delay) {
alive = true;
continue;
}
if (particle.age >= particle.lifetime) continue;
particle.age += delta;
if (particle.age >= particle.lifetime) continue;
alive = true;
particle.vx *= Math.exp(-0.5 * delta);
if (particle.vy < 0) {
// The upward burst is slowed by gravity...
particle.vy += 1400 * delta;
} else {
// ...then the paper drifts down at its own slow terminal speed
// instead of accelerating like a solid object.
particle.vy += (particle.terminalVelocity - particle.vy) * (1 - Math.exp(-2.6 * delta));
}
particle.x += (particle.vx + particle.wind) * delta;
particle.y += particle.vy * delta;
particle.rotation += particle.spin * delta;
if (particle.x < -80 || particle.x > width + 80 || particle.y > height + 120) {
continue;
}
const fadeStart = particle.lifetime - 1.8;
const alpha =
particle.age > fadeStart ? Math.max(0, 1 - (particle.age - fadeStart) / 1.8) : 1;
if (alpha <= 0) continue;
const flutter = Math.cos(particle.flutterPhase + particle.age * particle.flutterSpeed);
const sway = Math.sin(particle.age * particle.swaySpeed + particle.swayPhase);
const scaleX = 0.15 + Math.abs(flutter) * 0.85;
context.save();
context.translate(particle.x + sway * particle.swayAmount, particle.y + sway * 3.5);
context.rotate(particle.rotation + flutter * 0.5);
context.scale(scaleX, 1);
context.globalAlpha = alpha;
context.fillStyle = particle.color;
context.fillRect(
-particle.width / 2,
-particle.height / 2,
particle.width,
particle.height
);
context.restore();
}
if (alive) {
frame = requestAnimationFrame(paint);
} else {
context.clearRect(0, 0, width, height);
}
};
spawn();
frame = requestAnimationFrame(paint);
return () => {
cancelAnimationFrame(frame);
};
});
</script>
<canvas
bind:this={canvas}
aria-hidden="true"
class="pointer-events-none absolute inset-0 z-20 h-full w-full"
></canvas>
@@ -1,416 +0,0 @@
<script lang="ts">
import Badge from '$lib/components/ui/badge/badge.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import { launcherStream } from '$lib/global';
import { getReleaseStream, getVersion, isOsuCorrupted } from '$lib/osuUtil';
import { osuInstallationPath, patch } from '$lib/userSettings';
import { formatBytes } from '$lib/utils';
import {
AlertTriangle,
CheckCircle2,
ClipboardCopy,
Info,
LoaderCircle,
RefreshCw,
Wrench,
XCircle,
} from '@lucide/svelte';
import { invoke } from '@tauri-apps/api/core';
import { sileo } from 'sileo';
import { onMount } from 'svelte';
type InstallationDiagnostics = {
platform: string;
osVersion?: string;
osArch: string;
launcherVersion: string;
osuFolder: string;
resolvedFolder?: string;
osuWritable?: boolean;
patcherPath?: string;
patcherInstalled: boolean;
patcherFileCount: number;
patcherTotalSize: number;
hostNet8: boolean;
osuwinello: boolean;
wmctrl: boolean;
winePrefix?: string;
wineBinary?: string;
wineAppBundle?: string;
wineVersion?: string;
winePrefixArch?: string;
wineNet8?: boolean;
wineError?: string;
ffmpegStatus?: string;
logsDirectory: string;
};
type Check = {
label: string;
detail: string;
status: 'ok' | 'warning' | 'error' | 'info';
};
let { onRepair }: { onRepair?: () => void } = $props();
let checking = $state(true);
let checks = $state<Check[]>([]);
let lastChecked = $state<Date | undefined>(undefined);
let platformName = $state('');
let launcherVersionName = $state('');
let osVersion = $state('');
let osArch = $state('');
let logsDirectory = $state('');
const icons = {
ok: CheckCircle2,
warning: AlertTriangle,
error: XCircle,
info: Info,
};
const platformLabel = (platform: string) => {
switch (platform) {
case 'macos':
return 'macOS';
case 'windows':
return 'Windows';
case 'linux':
return 'Linux';
default:
return platform || 'unknown';
}
};
async function runChecks() {
checking = true;
checks = [];
try {
const folder = $osuInstallationPath;
const diagnostics = await invoke<InstallationDiagnostics>('installation_diagnostics', {
folder,
});
platformName = diagnostics.platform;
launcherVersionName = diagnostics.launcherVersion;
osVersion = diagnostics.osVersion ?? '';
osArch = diagnostics.osArch;
logsDirectory = diagnostics.logsDirectory;
const resolved = diagnostics.resolvedFolder;
// Only the experimental patcher build targets .NET 8.
const needsNet8 = $patch && $launcherStream === 'experimental';
const result: Check[] = [];
result.push({
label: 'Launcher',
detail: `${diagnostics.launcherVersion || 'unknown version'} · ${
diagnostics.osVersion || platformLabel(diagnostics.platform)
}${diagnostics.osArch ? ` (${diagnostics.osArch})` : ''}`,
status: 'ok',
});
if (!folder.trim()) {
result.push({
label: 'osu! installation',
detail: 'No installation path is configured. Run the setup wizard to pick osu!.',
status: 'error',
});
} else if (!resolved) {
result.push({
label: 'osu! installation',
detail: `The configured path could not be resolved: ${folder}`,
status: 'error',
});
} else {
result.push({
label: 'osu! installation',
detail: resolved,
status: 'ok',
});
try {
const [version, stream] = await Promise.all([
getVersion(resolved),
getReleaseStream(resolved),
]);
result.push({
label: 'osu! version',
detail: `${stream ?? 'unknown stream'} · ${version || 'version unavailable'}`,
status: version ? 'ok' : 'warning',
});
} catch (error) {
result.push({
label: 'osu! version',
detail: error instanceof Error ? error.message : String(error),
status: 'warning',
});
}
try {
const corrupted = await isOsuCorrupted(resolved);
result.push({
label: 'Game files',
detail: corrupted
? 'osu!.exe or a required file looks corrupted. Re-run the updater or setup.'
: 'The osu! executable passed the integrity check.',
status: corrupted ? 'error' : 'ok',
});
} catch (error) {
result.push({
label: 'Game files',
detail: error instanceof Error ? error.message : String(error),
status: 'warning',
});
}
}
if (typeof diagnostics.osuWritable === 'boolean') {
result.push({
label: 'osu! folder access',
detail: diagnostics.osuWritable
? 'The launcher can write to this folder, so updates and patcher downloads can be installed.'
: 'The launcher cannot write to this folder. Updates and patcher downloads will fail; check folder permissions.',
status: diagnostics.osuWritable ? 'ok' : 'error',
});
}
const selectedStream = $launcherStream;
const effectiveStream = diagnostics.platform === 'windows' ? selectedStream : 'stable';
result.push({
label: 'Patcher stream',
detail:
diagnostics.platform === 'windows'
? `${selectedStream} selected`
: `${effectiveStream} (selected: ${selectedStream}; macOS/Linux always use stable)`,
status: 'ok',
});
result.push({
label: 'Patcher',
detail: diagnostics.patcherInstalled
? `${diagnostics.patcherFileCount} file${
diagnostics.patcherFileCount === 1 ? '' : 's'
}, ${formatBytes(diagnostics.patcherTotalSize)} · ${diagnostics.patcherPath}`
: 'Patcher files are not installed yet. They will be downloaded automatically the next time you launch osu!.',
status: diagnostics.patcherInstalled ? 'ok' : $patch ? 'warning' : 'info',
});
result.push({
label: 'Logs',
detail: diagnostics.logsDirectory || 'The logs folder is unavailable.',
status: diagnostics.logsDirectory ? 'ok' : 'warning',
});
if (diagnostics.platform === 'macos') {
if (diagnostics.wineError) {
result.push({
label: 'Wine wrapper',
detail: diagnostics.wineError,
status: 'error',
});
} else {
result.push({
label: 'Wine prefix',
detail: diagnostics.winePrefix || 'Wine prefix not detected.',
status: diagnostics.winePrefix ? 'ok' : 'error',
});
result.push({
label: 'Wine runtime',
detail: diagnostics.wineBinary || 'No Wine loader found inside the wrapper.',
status: diagnostics.wineBinary ? 'ok' : 'error',
});
result.push({
label: 'Wine version',
detail: diagnostics.wineVersion || 'Could not determine the Wine version.',
status: diagnostics.wineVersion ? 'ok' : 'warning',
});
if (diagnostics.winePrefixArch) {
result.push({
label: 'Wine prefix architecture',
detail:
diagnostics.winePrefixArch === 'win64'
? '64-bit (win64)'
: diagnostics.winePrefixArch === 'win32'
? '32-bit (win32)'
: diagnostics.winePrefixArch,
status: 'ok',
});
}
if (diagnostics.wineAppBundle) {
result.push({
label: 'App wrapper',
detail: diagnostics.wineAppBundle,
status: 'ok',
});
}
result.push({
label: '.NET 8 in prefix (experimental)',
detail:
diagnostics.wineNet8 === true
? 'The Microsoft.WindowsDesktop.App 8 runtime was found.'
: 'The Windows .NET 8 Desktop Runtime was not found in this prefix. Only the experimental patcher stream needs it.',
status: diagnostics.wineNet8 === true ? 'ok' : needsNet8 ? 'error' : 'warning',
});
result.push({
label: 'FFmpeg (theme video)',
detail: diagnostics.ffmpegStatus
? diagnostics.ffmpegStatus
: 'FFmpeg was not found. Large theme videos will stay on WebKit’s software decoder and may stutter.',
status: diagnostics.ffmpegStatus ? 'ok' : 'warning',
});
}
} else if (diagnostics.platform === 'linux') {
result.push({
label: 'osu-winello',
detail: diagnostics.osuwinello
? 'osu-winello was found.'
: 'osu-winello was not found in PATH. Launching osu! may fail.',
status: diagnostics.osuwinello ? 'ok' : 'error',
});
result.push({
label: 'wmctrl',
detail: diagnostics.wmctrl
? 'wmctrl is available for window detection.'
: 'wmctrl is missing. The patcher may not detect the osu! window.',
status: diagnostics.wmctrl ? 'ok' : $patch ? 'warning' : 'info',
});
} else {
result.push({
label: '.NET 8 Desktop Runtime (experimental)',
detail: diagnostics.hostNet8
? 'Microsoft.WindowsDesktop.App 8 was found.'
: 'The .NET 8 Desktop Runtime was not found. Only the experimental patcher stream needs it.',
status: diagnostics.hostNet8 ? 'ok' : needsNet8 ? 'error' : 'warning',
});
}
checks = result;
lastChecked = new Date();
} catch (error) {
checks = [
{
label: 'Diagnostics',
detail: error instanceof Error ? error.message : String(error),
status: 'error',
},
];
} finally {
checking = false;
}
}
async function copyReport() {
const body = checks
.map((check) => `[${check.status.toUpperCase()}] ${check.label}: ${check.detail}`)
.join('\n');
const report = [
'EZPPLauncher diagnostics',
`Generated: ${new Date().toISOString()}`,
`Launcher: ${launcherVersionName || 'unknown'}`,
`Platform: ${osVersion || platformLabel(platformName)}${osArch ? ` (${osArch})` : ''}`,
`Logs: ${logsDirectory || 'unknown'}`,
'',
body,
].join('\n');
try {
await navigator.clipboard.writeText(report);
sileo.success({
title: 'Diagnostics copied',
description: 'Paste it into your support report.',
});
} catch {
sileo.error({ title: 'Could not copy diagnostics' });
}
}
onMount(runChecks);
</script>
<div class="divide-y divide-theme-800/50 border-t border-theme-800/50">
<div class="flex flex-wrap items-center justify-between gap-3 py-3">
<div class="min-w-0">
<p class="text-sm font-medium">System checks</p>
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
Inspect the selected osu! install, folder permissions, Wine wrapper, patcher, FFmpeg and
runtime dependencies.
</p>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-1.5">
{#if lastChecked}
<span class="mr-1 text-[10px] text-muted-foreground">
{lastChecked.toLocaleTimeString()}
</span>
{/if}
<Button
variant="outline"
size="sm"
class="border-theme-800 bg-theme-950"
onclick={copyReport}
>
<ClipboardCopy class="size-4" />
Copy report
</Button>
<Button
variant="outline"
size="sm"
class="border-theme-800 bg-theme-950"
onclick={runChecks}
disabled={checking}
>
<RefreshCw class={checking ? 'size-4 animate-spin' : 'size-4'} />
Refresh
</Button>
{#if onRepair}
<Button variant="secondary" size="sm" onclick={onRepair}>
<Wrench class="size-4" />
Repair setup
</Button>
{/if}
</div>
</div>
<div class="space-y-2 py-3">
{#if checking}
<div class="flex items-center gap-2 py-2 text-xs text-muted-foreground">
<LoaderCircle class="size-4 animate-spin" />
Checking your installation…
</div>
{:else}
{#each checks as check (check.label)}
{@const Icon = icons[check.status]}
<div
class="flex items-start gap-2.5 rounded-lg border border-theme-800/70 bg-theme-950/60 px-3 py-2"
>
<Icon
class="mt-0.5 size-4 shrink-0 {check.status === 'ok'
? 'text-emerald-400'
: check.status === 'warning'
? 'text-amber-300'
: check.status === 'error'
? 'text-destructive'
: 'text-muted-foreground'}"
/>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<p class="text-xs font-medium text-theme-100">{check.label}</p>
<Badge
variant={check.status === 'error'
? 'destructive'
: check.status === 'warning'
? 'secondary'
: 'outline'}
class="px-1.5 py-0 text-[9px] capitalize"
>
{check.status}
</Badge>
</div>
<p class="mt-0.5 break-words text-[10px] leading-relaxed text-muted-foreground">
{check.detail}
</p>
</div>
</div>
{/each}
{/if}
</div>
</div>
-337
View File
@@ -1,337 +0,0 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import { Checkbox } from '$lib/components/ui/checkbox';
import Input from '$lib/components/ui/input/input.svelte';
import { openLogsFolder } from '$lib/osuUtil';
import { formatBytes } from '$lib/utils';
import {
AlertTriangle,
Copy,
FileText,
FolderOpen,
LoaderCircle,
RefreshCw,
Search,
Terminal,
X,
} from '@lucide/svelte';
import { invoke } from '@tauri-apps/api/core';
import { sileo } from 'sileo';
import { onMount } from 'svelte';
type LogBundleInfo = {
path: string;
name: string;
size: number;
modified: number;
};
type LogBundleEntry = {
name: string;
content: string;
};
let { onclose }: { onclose: () => void } = $props();
let bundles = $state<LogBundleInfo[]>([]);
let selectedPath = $state<string | undefined>(undefined);
let entries = $state<LogBundleEntry[]>([]);
let selectedLog = $state('launcher.log');
let query = $state('');
let errorsOnly = $state(false);
let loadingBundles = $state(true);
let loadingEntries = $state(false);
let errorMessage = $state('');
const selectedBundle = $derived(bundles.find((bundle) => bundle.path === selectedPath));
const activeEntry = $derived(entries.find((entry) => entry.name === selectedLog));
const allLines = $derived(activeEntry?.content.split(/\r?\n/) ?? []);
const filteredLines = $derived.by(() => {
const text = query.trim().toLowerCase();
return allLines.filter((line) => {
if (errorsOnly && !/(error|warn|panic|fatal|failed)/i.test(line)) return false;
return !text || line.toLowerCase().includes(text);
});
});
const visibleLines = $derived(filteredLines.slice(0, 5000));
async function loadBundles() {
loadingBundles = true;
errorMessage = '';
try {
bundles = await invoke<LogBundleInfo[]>('list_log_bundles');
if (bundles.length === 0) {
selectedPath = undefined;
entries = [];
return;
}
const stillThere = bundles.some((bundle) => bundle.path === selectedPath);
const next = stillThere ? selectedPath : bundles[0].path;
selectedPath = next;
await loadBundle(next!);
} catch (error) {
errorMessage = error instanceof Error ? error.message : String(error);
} finally {
loadingBundles = false;
}
}
async function loadBundle(path: string) {
selectedPath = path;
loadingEntries = true;
errorMessage = '';
try {
entries = await invoke<LogBundleEntry[]>('read_log_bundle', { path });
if (!entries.some((entry) => entry.name === selectedLog)) {
selectedLog = entries[0]?.name ?? 'launcher.log';
}
} catch (error) {
entries = [];
errorMessage = error instanceof Error ? error.message : String(error);
} finally {
loadingEntries = false;
}
}
async function refresh() {
await loadBundles();
}
async function copyVisible() {
const text = filteredLines.join('\n');
try {
await navigator.clipboard.writeText(text);
sileo.success({
title: 'Log lines copied',
description: `${filteredLines.length} lines from ${selectedLog}.`,
});
} catch {
sileo.error({ title: 'Could not copy the log' });
}
}
async function openFolder() {
try {
await openLogsFolder();
} catch (error) {
sileo.error({
title: 'Could not open the logs folder',
description: error instanceof Error ? error.message : String(error),
});
}
}
function lineClass(line: string) {
if (/(error|panic|fatal|failed)/i.test(line)) return 'text-red-300';
if (/(warn|deprecated|missing)/i.test(line)) return 'text-amber-200';
if (/(info|started|loaded|saved)/i.test(line)) return 'text-theme-200';
return 'text-theme-300';
}
onMount(loadBundles);
</script>
<svelte:window
onkeydown={(event) => {
if (event.key === 'Escape') onclose();
}}
/>
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-5 backdrop-blur-sm"
role="presentation"
>
<div
class="flex h-[82vh] w-full max-w-6xl flex-col overflow-hidden rounded-xl border border-theme-700 bg-theme-950 shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="log-viewer-title"
>
<header class="flex shrink-0 items-center justify-between gap-3 border-b border-theme-800 p-3">
<div class="flex min-w-0 items-center gap-2">
<div class="flex size-8 items-center justify-center rounded-lg bg-primary/15 text-primary">
<Terminal class="size-4" />
</div>
<div class="min-w-0">
<h2 id="log-viewer-title" class="text-sm font-semibold">Log bundles</h2>
<p class="truncate text-[10px] text-muted-foreground">
Launcher, osu! and patcher output, grouped by session
</p>
</div>
</div>
<div class="flex shrink-0 items-center gap-1.5">
<Button
variant="outline"
size="sm"
class="border-theme-800 bg-theme-950"
onclick={openFolder}
>
<FolderOpen class="size-3.5" />
Folder
</Button>
<Button
variant="outline"
size="sm"
class="border-theme-800 bg-theme-950"
onclick={refresh}
disabled={loadingBundles}
>
<RefreshCw class={loadingBundles ? 'size-3.5 animate-spin' : 'size-3.5'} />
Refresh
</Button>
<Button variant="ghost" size="icon" class="size-8" title="Close logs" onclick={onclose}>
<X class="size-4" />
</Button>
</div>
</header>
<div class="grid min-h-0 flex-1 grid-cols-[230px_minmax(0,1fr)]">
<aside class="flex min-h-0 flex-col border-r border-theme-800 bg-black/20">
<div
class="flex shrink-0 items-center justify-between border-b border-theme-800 px-3 py-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"
>
<span>Bundles</span>
<span>{bundles.length}</span>
</div>
<div class="min-h-0 flex-1 overflow-y-auto p-1.5">
{#if loadingBundles}
<div class="flex items-center gap-2 p-3 text-xs text-muted-foreground">
<LoaderCircle class="size-4 animate-spin" />
Loading…
</div>
{:else if bundles.length === 0}
<div class="p-3 text-[11px] leading-relaxed text-muted-foreground">
No bundles yet. Logs are zipped on the next launcher restart or when a log reaches 5
MiB.
</div>
{:else}
{#each bundles as bundle (bundle.path)}
<button
type="button"
class="mb-1 w-full rounded-md border px-2 py-2 text-left transition {bundle.path ===
selectedPath
? 'border-primary/40 bg-primary/10'
: 'border-transparent hover:border-theme-800 hover:bg-theme-900/60'}"
onclick={() => loadBundle(bundle.path)}
>
<span class="flex items-center gap-1.5">
<FileText class="size-3.5 shrink-0 text-primary" />
<span class="truncate text-[11px] font-medium text-theme-100">
{bundle.name}
</span>
</span>
<span class="mt-1 block pl-5 text-[9px] text-muted-foreground">
{new Date(bundle.modified).toLocaleString()} · {formatBytes(bundle.size)}
</span>
</button>
{/each}
{/if}
</div>
</aside>
<main class="flex min-h-0 min-w-0 flex-col">
{#if errorMessage}
<div
class="flex items-center gap-2 border-b border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive"
>
<AlertTriangle class="size-4" />
{errorMessage}
</div>
{/if}
<div class="flex shrink-0 flex-wrap items-center gap-1.5 border-b border-theme-800 p-2">
{#if entries.length > 0}
{#each entries as entry (entry.name)}
<Button
variant="ghost"
size="sm"
class="h-7 text-[10px] {selectedLog === entry.name
? 'bg-primary/15 text-primary'
: 'text-muted-foreground'}"
onclick={() => (selectedLog = entry.name)}
>
{entry.name}
</Button>
{/each}
{/if}
<div class="relative ml-auto w-52">
<Search
class="pointer-events-none absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground"
/>
<Input
class="h-7 border-theme-800 bg-theme-950 pl-7 text-[10px]"
placeholder="Filter lines…"
bind:value={query}
/>
</div>
<div
class="flex items-center gap-1.5 rounded-md border border-theme-800 bg-theme-950 px-2 py-1"
>
<Checkbox id="log-errors-only" bind:checked={errorsOnly} />
<label for="log-errors-only" class="text-[10px] text-muted-foreground">
Errors only
</label>
</div>
<Button
variant="outline"
size="sm"
class="h-7 border-theme-800 bg-theme-950 text-[10px]"
onclick={copyVisible}
disabled={filteredLines.length === 0}
>
<Copy class="size-3" />
Copy
</Button>
</div>
<div class="min-h-0 flex-1 overflow-auto bg-black/25">
{#if loadingEntries}
<div
class="flex h-full items-center justify-center gap-2 text-xs text-muted-foreground"
>
<LoaderCircle class="size-4 animate-spin" />
Reading bundle…
</div>
{:else if !selectedBundle}
<div
class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted-foreground"
>
<Terminal class="size-8 opacity-40" />
<p class="text-xs">Choose a log bundle to inspect it.</p>
</div>
{:else if filteredLines.length === 0}
<div
class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted-foreground"
>
<Search class="size-6 opacity-40" />
<p class="text-xs">No lines match this filter.</p>
</div>
{:else}
<div class="p-3 font-mono text-[10px] leading-relaxed">
{#each visibleLines as line, index (index)}
<div class="whitespace-pre-wrap break-words {lineClass(line)}">{line || ' '}</div>
{/each}
{#if filteredLines.length > visibleLines.length}
<p class="mt-2 text-amber-200">
Showing the first {visibleLines.length} matching lines. Narrow the filter to see more.
</p>
{/if}
</div>
{/if}
</div>
<footer
class="flex shrink-0 items-center justify-between border-t border-theme-800 px-3 py-1.5 text-[9px] text-muted-foreground"
>
<span>{filteredLines.length} matching lines</span>
<span>
{selectedBundle
? `${selectedBundle.name} · ${formatBytes(selectedBundle.size)}`
: 'No bundle selected'}
</span>
</footer>
</main>
</div>
</div>
</div>
@@ -1,88 +0,0 @@
<script module lang="ts">
let previewCount = 0;
</script>
<script lang="ts">
import { onMount } from 'svelte';
type Props = {
family: string;
src: string;
weight?: string;
style?: 'normal' | 'italic';
text?: string;
class?: string;
};
let {
family,
src,
weight = '400',
style = 'normal',
text = family,
class: className = '',
}: Props = $props();
let host = $state<HTMLSpanElement>();
let visible = $state(false);
let renderedFamily = $state<string>();
let face: FontFace | undefined;
onMount(() => {
if (!host) return;
const observer = new IntersectionObserver(
(entries) => {
if (!entries.some((entry) => entry.isIntersecting)) return;
visible = true;
observer.disconnect();
},
{ rootMargin: '180px 0px' }
);
observer.observe(host);
return () => observer.disconnect();
});
$effect(() => {
if (!visible || !src) return;
let cancelled = false;
const previewFamily = `EZPPSourcePreview_${previewCount++}`;
void (async () => {
try {
const candidate = new FontFace(previewFamily, `url("${src}")`, {
weight,
style,
display: 'swap',
});
const loaded = await candidate.load();
if (cancelled) return;
document.fonts.add(loaded);
face = loaded;
renderedFamily = previewFamily;
} catch {
// A preview failure should leave the row readable in the fallback font.
}
})();
return () => {
cancelled = true;
if (face && face.family === previewFamily) {
document.fonts.delete(face);
face = undefined;
}
if (renderedFamily === previewFamily) renderedFamily = undefined;
};
});
</script>
<span
bind:this={host}
class={className}
style:font-family={renderedFamily
? `"${renderedFamily}", ui-sans-serif, system-ui, sans-serif`
: undefined}
style:font-weight={weight}
style:font-style={style}
>
{text}
</span>
File diff suppressed because it is too large. Load diff
@@ -1,426 +0,0 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import { Checkbox } from '$lib/components/ui/checkbox';
import Input from '$lib/components/ui/input/input.svelte';
import Label from '$lib/components/ui/label/label.svelte';
import * as Select from '$lib/components/ui/select';
import Slider from '$lib/components/ui/slider/slider.svelte';
import Textarea from '$lib/components/ui/textarea/textarea.svelte';
import type { ThemeFontChoice } from '$lib/themeFonts';
import type { ThemeV2Element } from '$lib/themeV2';
import { Image as ImageIcon, RefreshCw, Type } from '@lucide/svelte';
let {
element,
fontOptions,
onreplace,
}: {
element: ThemeV2Element;
fontOptions: ThemeFontChoice[];
onreplace: (kind: 'image' | 'video') => void;
} = $props();
const field = 'h-8 border-theme-800 bg-theme-950 text-xs text-theme-50';
const label = 'text-[10px] font-medium uppercase tracking-wide text-muted-foreground';
</script>
<div class="space-y-4 p-4">
<div class="flex items-center gap-2 border-b border-theme-800 pb-3">
<div class="flex size-8 items-center justify-center rounded-md bg-primary/20 text-primary">
{#if element.type === 'text'}
<Type class="size-4" />
{:else}
<ImageIcon class="size-4" />
{/if}
</div>
<div class="min-w-0">
<p class="truncate text-xs font-semibold capitalize text-theme-50">{element.type} layer</p>
<p class="truncate font-mono text-[10px] text-muted-foreground">{element.id}</p>
</div>
</div>
<div class="space-y-1">
<Label for="element-name" class={label}>Layer name</Label>
<Input
id="element-name"
class={field}
value={element.name ?? ''}
placeholder="Optional label, for example Logo"
oninput={(event) => (element.name = (event.currentTarget as HTMLInputElement).value)}
/>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="element-x" class={label}>X</Label>
<Input id="element-x" class={field} type="number" step="1" bind:value={element.x} />
</div>
<div class="space-y-1">
<Label for="element-y" class={label}>Y</Label>
<Input id="element-y" class={field} type="number" step="1" bind:value={element.y} />
</div>
<div class="space-y-1">
<Label for="element-width" class={label}>Width</Label>
<Input
id="element-width"
class={field}
type="number"
min="24"
max="1000"
bind:value={element.width}
/>
</div>
<div class="space-y-1">
<Label for="element-height" class={label}>Height</Label>
<Input
id="element-height"
class={field}
type="number"
min="24"
max="650"
bind:value={element.height}
/>
</div>
<div class="space-y-1">
<Label for="element-rotation" class={label}>Rotation</Label>
<Input
id="element-rotation"
class={field}
type="number"
min="-360"
max="360"
bind:value={element.rotation}
/>
</div>
<div class="space-y-1">
<div class="flex items-center justify-between">
<Label class={label}>Opacity</Label>
<span class="text-[10px] text-muted-foreground"
>{Math.round((element.opacity ?? 1) * 100)}%</span
>
</div>
<Slider
min={0}
max={1}
step={0.01}
value={element.opacity ?? 1}
onValueChange={(value) => (element.opacity = value)}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-2 border-t border-theme-800 pt-3">
<div class="flex items-center gap-2 rounded-md border border-theme-800 bg-black/20 px-2 py-1.5">
<Checkbox
id="element-hidden"
checked={element.hidden === true}
onCheckedChange={(checked) => (element.hidden = checked === true)}
/>
<Label for="element-hidden" class="text-[10px] font-normal text-muted-foreground">
Hidden
</Label>
</div>
<div class="flex items-center gap-2 rounded-md border border-theme-800 bg-black/20 px-2 py-1.5">
<Checkbox
id="element-locked"
checked={element.locked === true}
onCheckedChange={(checked) => (element.locked = checked === true)}
/>
<Label for="element-locked" class="text-[10px] font-normal text-muted-foreground">
Locked
</Label>
</div>
</div>
{#if element.type === 'text'}
<div class="space-y-3 border-t border-theme-800 pt-3">
<div class="space-y-1">
<Label for="element-text" class={label}>Text</Label>
<Textarea
id="element-text"
class="min-h-20 resize-y border-theme-800 bg-theme-950 text-xs normal-case tracking-normal text-theme-50"
bind:value={element.text}
/>
</div>
<div class="space-y-1">
<Label for="element-font" class={label}>Font</Label>
<Select.Root
type="single"
value={element.fontFamily ?? 'Sora Variable'}
onValueChange={(value) => {
if (value) element.fontFamily = value;
}}
>
<Select.Trigger
id="element-font"
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
>
{element.fontFamily ?? 'Sora Variable'}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
{#each fontOptions as font (font.family)}
<Select.Item value={font.family} label={`${font.label} · ${font.category}`} />
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="element-font-size" class={label}>Size</Label>
<Input
id="element-font-size"
class={field}
type="number"
min="6"
max="240"
bind:value={element.fontSize}
/>
</div>
<div class="space-y-1">
<Label for="element-font-weight" class={label}>Weight</Label>
<Select.Root
type="single"
value={String(element.fontWeight ?? 400)}
onValueChange={(value) => {
if (value) element.fontWeight = Number(value);
}}
>
<Select.Trigger
id="element-font-weight"
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
>
{element.fontWeight ?? 400}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
{#each [100, 200, 300, 400, 500, 600, 700, 800, 900] as weight (weight)}
<Select.Item value={String(weight)} label={String(weight)} />
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="element-align" class={label}>Align</Label>
<Select.Root
type="single"
value={element.align ?? 'left'}
onValueChange={(value) => {
if (value) element.align = value as 'left' | 'center' | 'right';
}}
>
<Select.Trigger
id="element-align"
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
>
{element.align ?? 'left'}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
<Select.Item value="left" label="Left" />
<Select.Item value="center" label="Center" />
<Select.Item value="right" label="Right" />
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="element-vertical-align" class={label}>Vertical</Label>
<Select.Root
type="single"
value={element.verticalAlign ?? 'top'}
onValueChange={(value) => {
if (value) element.verticalAlign = value as 'top' | 'center' | 'bottom';
}}
>
<Select.Trigger
id="element-vertical-align"
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
>
{element.verticalAlign ?? 'top'}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
<Select.Item value="top" label="Top" />
<Select.Item value="center" label="Middle" />
<Select.Item value="bottom" label="Bottom" />
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="element-line-height" class={label}>Line height</Label>
<Input
id="element-line-height"
class={field}
type="number"
min="0.7"
max="3"
step="0.05"
bind:value={element.lineHeight}
/>
</div>
<div class="space-y-1">
<Label for="element-letter-spacing" class={label}>Spacing</Label>
<Input
id="element-letter-spacing"
class={field}
type="number"
min="-20"
max="40"
step="0.5"
bind:value={element.letterSpacing}
/>
</div>
</div>
<div class="space-y-1">
<Label for="element-color" class={label}>Color</Label>
<div class="flex items-center gap-2">
<Input
class="h-8 w-10 shrink-0 cursor-pointer border-theme-800 bg-theme-950 p-0.5"
type="color"
bind:value={element.color}
/>
<Input id="element-color" class="{field} flex-1 font-mono" bind:value={element.color} />
</div>
</div>
</div>
{:else if element.type === 'image' || element.type === 'video'}
<div class="space-y-3 border-t border-theme-800 pt-3">
<div class="rounded-md border border-theme-800 bg-black/20 p-2">
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">Asset</p>
<p class="mt-1 truncate font-mono text-[10px] text-theme-100">{element.src}</p>
</div>
<Button
variant="secondary"
class="h-8 w-full gap-2 text-xs"
onclick={() => onreplace(element.type as 'image' | 'video')}
>
<RefreshCw class="size-3.5" />
Replace {element.type}
</Button>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="element-fit" class={label}>Fit</Label>
<Select.Root
type="single"
value={element.fit ?? 'cover'}
onValueChange={(value) => {
if (value) element.fit = value as 'cover' | 'contain';
}}
>
<Select.Trigger
id="element-fit"
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
>
{element.fit ?? 'cover'}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
<Select.Item value="cover" label="Cover" />
<Select.Item value="contain" label="Contain" />
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="element-radius" class={label}>Radius</Label>
<Input
id="element-radius"
class={field}
type="number"
min="0"
max="999"
bind:value={element.radius}
/>
</div>
</div>
{#if element.type === 'video'}
<div class="flex items-start gap-2 rounded-md border border-theme-800 bg-black/20 p-2">
<Checkbox
id="element-muted"
class="mt-0.5"
checked={element.muted !== false}
onCheckedChange={(checked) => {
element.muted = checked === true;
}}
/>
<div class="space-y-0.5">
<Label for="element-muted" class="text-[10px] font-normal text-theme-100">
Mute video
</Label>
<p class="text-[10px] leading-relaxed text-muted-foreground">
Unmuted videos may wait for a click before they start, because of autoplay policies.
</p>
</div>
</div>
<p class="text-[10px] leading-relaxed text-muted-foreground">
Videos autoplay and loop. WebM keeps theme packages small; MP4 also works.
</p>
{/if}
</div>
{:else}
<div class="space-y-3 border-t border-theme-800 pt-3">
<div class="space-y-1">
<Label for="element-shape" class={label}>Shape</Label>
<Select.Root
type="single"
value={element.shape}
onValueChange={(value) => {
if (value) element.shape = value as 'rectangle' | 'ellipse' | 'line';
}}
>
<Select.Trigger
id="element-shape"
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
>
{element.shape}
</Select.Trigger>
<Select.Content class="border-theme-800 bg-theme-950">
<Select.Item value="rectangle" label="Rectangle" />
<Select.Item value="ellipse" label="Ellipse" />
<Select.Item value="line" label="Line" />
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="element-fill" class={label}>Fill</Label>
<div class="flex items-center gap-2">
<Input
class="h-8 w-10 shrink-0 cursor-pointer border-theme-800 bg-theme-950 p-0.5"
type="color"
bind:value={element.fill}
/>
<Input id="element-fill" class="{field} flex-1 font-mono" bind:value={element.fill} />
</div>
</div>
{#if element.shape !== 'ellipse'}
<div class="space-y-1">
<Label for="element-shape-radius" class={label}>Corner radius</Label>
<Input
id="element-shape-radius"
class={field}
type="number"
min="0"
max="999"
bind:value={element.radius}
/>
</div>
{/if}
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label for="element-border-width" class={label}>Border</Label>
<Input
id="element-border-width"
class={field}
type="number"
min="0"
max="40"
bind:value={element.borderWidth}
/>
</div>
<div class="space-y-1">
<Label for="element-border-color" class={label}>Border color</Label>
<Input
id="element-border-color"
class="h-8 w-full cursor-pointer border-theme-800 bg-theme-950 p-0.5"
type="color"
bind:value={element.borderColor}
/>
</div>
</div>
</div>
{/if}
</div>
-199
View File
@@ -1,199 +0,0 @@
<script lang="ts">
import Logo from '$assets/logo.png';
import ThemeV2Background from '$lib/components/theme/ThemeV2Background.svelte';
import ThemeV2Elements from '$lib/components/theme/ThemeV2Elements.svelte';
import { fontStack } from '$lib/themeFonts';
import { readableTextColor, themeFontFaceCss, type ThemeV2Definition } from '$lib/themeV2';
import { Brush, House, Music, Paintbrush, Play, Settings, User, Wifi } from '@lucide/svelte';
let {
definition,
assets = '',
assetOverrides = {},
selectedIds = [],
interactive = false,
scale = 1,
onselect,
onselectmany,
onmove,
onpatch,
oncontext,
}: {
definition: ThemeV2Definition;
assets?: string;
assetOverrides?: Record<string, string>;
selectedIds?: string[];
interactive?: boolean;
scale?: number;
onselect?: (id: string | undefined, additive?: boolean) => void;
onselectmany?: (ids: string[], additive?: boolean) => void;
onmove?: (updates: { id: string; x: number; y: number }[]) => void;
onpatch?: (id: string, patch: Record<string, unknown>) => void;
oncontext?: (id: string | undefined, event: MouseEvent) => void;
} = $props();
const bodyFont = $derived(definition.typography?.body);
const headingFont = $derived(definition.typography?.heading);
const accent = $derived(definition.accentColor?.trim() || '#ff006e');
const accentText = $derived(readableTextColor(accent));
const stageStyle = $derived(
[
`font-family:${fontStack(bodyFont)}`,
`--primary:${accent}`,
`--primary-foreground:${accentText}`,
`--ring:${accent}`,
`--color-primary:${accent}`,
`--color-primary-foreground:${accentText}`,
`--color-primary-500:${accent}`,
`--color-ring:${accent}`,
].join(';')
);
const fontFaces = $derived(themeFontFaceCss(definition, assets, assetOverrides));
const elements = $derived(definition.elements ?? []);
</script>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html `<style>${fontFaces}</style>`}
<div
class="theme-stage relative h-[650px] w-[1000px] overflow-hidden bg-theme-950 text-theme-50"
style={stageStyle}
role="presentation"
>
<ThemeV2Background {definition} {assets} {assetOverrides} preview />
<div class="relative z-10 grid h-full grid-cols-[0.085fr_1fr]">
<aside
class="flex flex-col items-center gap-2 border-r border-theme-900 bg-black/40 p-3 backdrop-blur-sm"
>
<div class="rounded-[1.1rem] bg-primary/30 p-1.5 ring-1 ring-inset ring-white/15">
<img class="pointer-events-none size-[43px]" src={Logo} alt="" draggable="false" />
</div>
<span
class="rounded-full bg-primary px-2 py-0 text-[8px] font-semibold text-primary-foreground"
>
3.2.7
</span>
<div
class="mt-3 flex size-12 items-center justify-center rounded-[0.85rem] bg-primary/50 p-3 ring-1 ring-inset ring-white/15"
>
<House class="size-5 text-theme-200" />
</div>
<div
class="mt-3 flex size-12 items-center justify-center rounded-[0.85rem] border border-black/20 bg-black/20 p-3 ring-1 ring-inset ring-white/15"
>
<Paintbrush class="size-5 text-theme-200" />
</div>
<div
class="mt-3 flex size-12 items-center justify-center rounded-[0.85rem] border border-black/20 bg-black/20 p-3 ring-1 ring-inset ring-white/15"
>
<Settings class="size-5 text-theme-200" />
</div>
<div
class="mt-auto flex size-10 items-center justify-center overflow-hidden rounded-full bg-theme-900"
>
<User class="size-5 text-muted-foreground" />
</div>
</aside>
<section class="relative flex min-w-0 flex-col">
<div class="flex min-h-0 flex-1 flex-col">
<div class="mt-12 mb-auto flex flex-col px-6">
<span
class="text-4xl font-bold drop-shadow-lg"
style="font-family:{fontStack(headingFont, bodyFont)}"
>
EZPPLauncher
</span>
<span class="font-semibold text-muted-foreground drop-shadow-lg">Hello Guest!</span>
</div>
<div class="relative h-50 shrink-0">
<div
class="absolute left-0 top-0 m-1 flex h-48 w-72 flex-col items-center justify-center gap-3 rounded-md bg-black/40 p-3 ring-1 ring-inset ring-white/5 backdrop-blur-sm"
>
<div class="flex flex-col items-center gap-1 text-center">
<span class="text-sm font-semibold text-theme-50">See your stats here</span>
<span class="text-xs text-muted-foreground">
Log in with your EZPPFarm account to track your rank, PP, accuracy and playcount.
</span>
</div>
<div class="flex w-full flex-col gap-2">
<div
class="flex h-7 w-full items-center justify-center rounded-md bg-primary text-xs font-medium text-primary-foreground"
>
Log In
</div>
<div
class="flex h-7 w-full items-center justify-center rounded-md border border-theme-800 text-xs font-medium text-muted-foreground"
>
Register
</div>
</div>
</div>
</div>
</div>
<div
class="relative z-10 flex items-center justify-between border-t border-t-theme-900 bg-black/40 px-8 py-4 backdrop-blur-sm"
>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<Music class="size-3.5 text-blue-400" />
<span class="text-[11px] font-medium text-white/70">1.2k</span>
</div>
<div class="flex items-center gap-2">
<Brush class="size-3.5 text-amber-400" />
<span class="text-[11px] font-medium text-white/70">42</span>
</div>
<div class="flex items-center gap-2">
<Wifi class="size-3.5 text-emerald-400" />
<span class="text-[11px] font-medium text-white/70">19ms</span>
</div>
<div class="h-4 w-px bg-white/10"></div>
<span
class="rounded-full border border-white/6 bg-white/10 px-2 py-0.5 text-[10px] text-white/50"
>
stable
</span>
<span
class="rounded-full border border-white/6 bg-white/10 px-2 py-0.5 font-mono text-[10px] text-white/50"
>
20260101.0
</span>
<span
class="rounded-full border border-primary/20 bg-primary/20 px-2 py-0.5 text-[10px] text-primary"
>
Default skin
</span>
</div>
<div class="flex h-12 items-center rounded-full bg-primary shadow-lg shadow-primary/25">
<div class="relative flex size-12 shrink-0 items-center justify-center">
<div
class="relative z-10 flex size-9 items-center justify-center rounded-full bg-black/30"
>
<Play class="ml-0.5 size-4 fill-current text-primary-foreground" />
</div>
</div>
<div class="w-52 pl-1 pr-6 text-center">
<p class="text-sm font-bold leading-tight text-primary-foreground">Launch</p>
</div>
</div>
</div>
</section>
</div>
<ThemeV2Elements
{elements}
{assets}
{assetOverrides}
{interactive}
{selectedIds}
{scale}
{onselect}
{onselectmany}
{onmove}
{onpatch}
{oncontext}
/>
</div>
@@ -1,252 +0,0 @@
<script lang="ts">
import { custom_theme_volume } from '$lib/global';
import { themeMediaSource, themeVideoLifecycle } from '$lib/themeMedia';
import type { ThemeV2Definition } from '$lib/themeV2';
import { themeAssetUrl } from '$lib/themeV2';
import { reduceAnimations } from '$lib/userSettings';
import { trackMediaContainer } from '$lib/utils';
let {
definition,
assets = '',
assetOverrides = {},
preview = false,
}: {
definition: ThemeV2Definition;
assets?: string;
assetOverrides?: Record<string, string>;
preview?: boolean;
} = $props();
let audioElement = $state<HTMLAudioElement | undefined>(undefined);
const assetUrl = (path: string | undefined) => {
if (!path) return '';
return assetOverrides[path] ?? themeAssetUrl(assets, path);
};
const background = $derived(definition.background);
const overlay = $derived(definition.overlay);
const effects = $derived(definition.effects);
const random = (seed: number) => {
const value = Math.sin(seed * 12.9898) * 43758.5453;
return value - Math.floor(value);
};
const particles = $derived.by(() => {
if ($reduceAnimations) return [];
if (!effects || effects.type === 'none') return [];
const count = Math.min(120, Math.max(0, Math.round(effects.density ?? 30)));
const speed = Math.max(0.2, effects.speed ?? 1);
return Array.from({ length: count }, (_, index) => {
const value = (offset: number) => random(index + 1 + offset);
const isStar = effects.type === 'stars';
const size = isStar ? 1 + value(1) * 2.5 : 4 + value(1) * 7;
const duration = (isStar ? 2 + value(2) * 3 : 9 + value(2) * 11) / speed;
return {
id: index,
left: value(3) * 100,
top: isStar ? value(4) * 100 : value(4) * 15,
size,
duration,
delay: value(5) * duration,
drift: -90 + value(6) * 180,
opacity: 0.25 + value(7) * 0.65,
rotate: value(8) * 360,
};
});
});
function trackMedia(node: HTMLElement) {
if (preview) return {};
return trackMediaContainer(node);
}
// `muted` is not reliably reflected after load, so keep the live property in
// sync when the theme toggles it in the editor.
function applyMuted(node: HTMLVideoElement, muted: boolean) {
node.muted = muted;
return {
update(value: boolean) {
node.muted = value;
},
};
}
$effect(() => {
if (!audioElement || !definition.audio) return;
audioElement.volume = $custom_theme_volume;
if (!preview) {
audioElement.play().catch(() => {
// Autoplay can be rejected until the window has been interacted with.
});
}
});
</script>
<div class="theme-v2 absolute inset-0 overflow-hidden" use:trackMedia>
{#if background.type === 'color'}
<div
class="absolute inset-0"
style="background-color:{background.color};opacity:{background.opacity ?? 1}"
></div>
{:else if background.type === 'gradient'}
<div
class="absolute inset-0"
style="background-image:linear-gradient({background.angle ??
135}deg, {background.from}, {background.to});opacity:{background.opacity ?? 1}"
></div>
{:else if background.type === 'image'}
{#if background.src}
<img
class="absolute inset-0 h-full w-full"
src={assetUrl(background.src)}
alt=""
aria-hidden="true"
style="object-fit:{background.fit ?? 'cover'};opacity:{background.opacity ??
1};filter:{background.blur
? `blur(${background.blur}px)`
: 'none'};transform:{background.blur ? 'scale(1.05)' : 'none'};"
/>
{/if}
{:else if background.type === 'video'}
{#if background.src}
<video
class="absolute inset-0 h-full w-full object-cover"
use:themeMediaSource={assetUrl(background.src)}
use:themeVideoLifecycle
autoplay
loop
muted={background.muted !== false}
use:applyMuted={background.muted !== false}
playsinline
disablepictureinpicture
style="object-fit:{background.fit ?? 'cover'};opacity:{background.opacity ??
1};filter:{background.blur
? `blur(${background.blur}px)`
: 'none'};transform:{background.blur ? 'scale(1.05)' : 'none'};"
></video>
{/if}
{/if}
{#if overlay && (overlay.opacity ?? 0) > 0}
<div
class="absolute inset-0 pointer-events-none"
style="background-color:{overlay.color ?? '#000000'};opacity:{overlay.opacity ?? 0}"
></div>
{/if}
{#if overlay && (overlay.vignette ?? 0) > 0}
<div
class="vignette absolute inset-0 pointer-events-none"
style="opacity:{overlay.vignette ?? 0}"
></div>
{/if}
{#if effects && effects.type !== 'none'}
<div class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
{#each particles as particle (particle.id)}
<span
class="particle {effects.type}"
style="left:{particle.left}%;top:{particle.top}%;width:{particle.size}px;height:{particle.size}px;opacity:{particle.opacity};--particle-color:{effects.color ??
'#ffffff'};--particle-drift:{particle.drift}px;--particle-duration:{particle.duration}s;--particle-delay:-{particle.delay}s;--particle-rotate:{particle.rotate}deg;animation-duration:{particle.duration}s;animation-delay:-{particle.delay}s;"
></span>
{/each}
</div>
{/if}
{#if definition.audio && !preview}
<audio
bind:this={audioElement}
use:themeMediaSource={assetUrl(definition.audio.src)}
loop={definition.audio.loop !== false}
autoplay
></audio>
{/if}
</div>
<style lang="scss">
.vignette {
background: radial-gradient(ellipse at center, transparent 38%, rgba(0, 0, 0, 0.92) 100%);
}
.particle {
position: absolute;
display: block;
background: var(--particle-color);
animation-timing-function: linear;
animation-iteration-count: infinite;
will-change: transform, opacity;
&.snow {
border-radius: 9999px;
filter: blur(0.5px);
animation-name: v2-snow;
}
&.petals {
border-radius: 70% 0 70% 0;
animation-name: v2-petals;
}
&.bubbles {
border-radius: 9999px;
border: 1px solid var(--particle-color);
background: color-mix(in srgb, var(--particle-color) 16%, transparent);
animation-name: v2-bubbles;
}
&.stars {
border-radius: 9999px;
box-shadow: 0 0 6px var(--particle-color);
animation-name: v2-stars;
animation-timing-function: ease-in-out;
}
}
@keyframes v2-snow {
from {
transform: translate3d(0, -12vh, 0);
}
to {
transform: translate3d(var(--particle-drift), 112vh, 0);
}
}
@keyframes v2-petals {
from {
transform: translate3d(0, -12vh, 0) rotate(var(--particle-rotate));
}
50% {
transform: translate3d(calc(var(--particle-drift) * 0.55), 50vh, 0)
rotate(calc(var(--particle-rotate) + 180deg));
}
to {
transform: translate3d(var(--particle-drift), 112vh, 0)
rotate(calc(var(--particle-rotate) + 360deg));
}
}
@keyframes v2-bubbles {
from {
transform: translate3d(0, 110vh, 0);
}
to {
transform: translate3d(var(--particle-drift), -12vh, 0);
}
}
@keyframes v2-stars {
0%,
100% {
opacity: 0.15;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
</style>
@@ -1,497 +0,0 @@
<script lang="ts">
import { fontStack } from '$lib/themeFonts';
import { themeMediaSource, themeVideoLifecycle } from '$lib/themeMedia';
import { themeAssetUrl, type ThemeV2Element } from '$lib/themeV2';
let {
elements = [],
assets = '',
assetOverrides = {},
interactive = false,
selectedIds = [],
scale = 1,
onselect,
onselectmany,
onmove,
onpatch,
oncontext,
}: {
elements?: ThemeV2Element[];
assets?: string;
assetOverrides?: Record<string, string>;
interactive?: boolean;
selectedIds?: string[];
scale?: number;
onselect?: (id: string | undefined, additive?: boolean) => void;
onselectmany?: (ids: string[], additive?: boolean) => void;
onmove?: (updates: { id: string; x: number; y: number }[]) => void;
onpatch?: (id: string, patch: Record<string, unknown>) => void;
oncontext?: (id: string | undefined, event: MouseEvent) => void;
} = $props();
const assetUrl = (path: string) => assetOverrides[path] ?? themeAssetUrl(assets, path);
const visibleElements = $derived(elements.filter((element) => !element.hidden));
// Keep the live muted property in sync, including live editor toggles.
function applyMuted(node: HTMLVideoElement, muted: boolean) {
node.muted = muted;
return {
update(value: boolean) {
node.muted = value;
},
};
}
type ElementOrigin = {
id: string;
x: number;
y: number;
width: number;
height: number;
};
type DragState = {
ids: string[];
startX: number;
startY: number;
origins: ElementOrigin[];
};
type ResizeDirection = 'nw' | 'ne' | 'sw' | 'se';
type ResizeState = {
id: string;
direction: ResizeDirection;
startX: number;
startY: number;
originX: number;
originY: number;
originWidth: number;
originHeight: number;
};
type MarqueeState = {
startX: number;
startY: number;
x: number;
y: number;
width: number;
height: number;
additive: boolean;
};
const resizeDirections: ResizeDirection[] = ['nw', 'ne', 'sw', 'se'];
const SNAP_DISTANCE = 6;
let overlay = $state<HTMLDivElement | undefined>(undefined);
let drag = $state<DragState | null>(null);
let resize = $state<ResizeState | null>(null);
let marquee = $state<MarqueeState | null>(null);
let snapGuides = $state<{ x?: number; y?: number }>({});
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
const selectedSet = $derived(new Set(selectedIds));
const isSelected = (id: string) => interactive && selectedSet.has(id);
const modifierPressed = (event: PointerEvent | KeyboardEvent) =>
event.shiftKey || event.metaKey || event.ctrlKey;
function selectElement(id: string | undefined, additive = false) {
onselect?.(id, additive);
}
function beginDrag(event: PointerEvent, element: ThemeV2Element) {
if (!interactive || event.button !== 0) return;
if ((event.target as HTMLElement).dataset.resize) return;
event.preventDefault();
event.stopPropagation();
const additive = modifierPressed(event);
if (additive) {
selectElement(element.id, true);
return;
}
if (element.locked) {
selectElement(element.id, false);
return;
}
const keepGroup = selectedSet.has(element.id) && selectedIds.length > 1;
if (!keepGroup) selectElement(element.id, false);
const candidateIds = keepGroup ? [...selectedIds] : [element.id];
const origins = candidateIds
.map((id) => elements.find((candidate) => candidate.id === id))
.filter(
(candidate): candidate is ThemeV2Element => candidate !== undefined && !candidate.locked
)
.map((candidate) => ({
id: candidate.id,
x: candidate.x,
y: candidate.y,
width: candidate.width,
height: candidate.height,
}));
if (origins.length === 0) return;
drag = {
ids: origins.map((origin) => origin.id),
startX: event.clientX,
startY: event.clientY,
origins,
};
snapGuides = {};
window.addEventListener('pointermove', moveDrag);
window.addEventListener('pointerup', endDrag, { once: true });
}
function groupBounds(origins: ElementOrigin[]) {
const left = Math.min(...origins.map((origin) => origin.x));
const top = Math.min(...origins.map((origin) => origin.y));
const right = Math.max(...origins.map((origin) => origin.x + origin.width));
const bottom = Math.max(...origins.map((origin) => origin.y + origin.height));
return { left, top, right, bottom, centerX: (left + right) / 2, centerY: (top + bottom) / 2 };
}
function closestSnap(
lines: number[],
candidates: number[]
): { delta: number; guide: number } | undefined {
let best: { delta: number; guide: number } | undefined;
for (const line of lines) {
for (const candidate of candidates) {
const delta = candidate - line;
if (Math.abs(delta) > SNAP_DISTANCE) continue;
if (!best || Math.abs(delta) < Math.abs(best.delta)) {
best = { delta, guide: candidate };
}
}
}
return best;
}
function moveDrag(event: PointerEvent) {
if (!drag) return;
const bounds = groupBounds(drag.origins);
const rawDx = (event.clientX - drag.startX) / scale;
const rawDy = (event.clientY - drag.startY) / scale;
let dx = clamp(rawDx, -bounds.left, 1000 - bounds.right);
let dy = clamp(rawDy, -bounds.top, 650 - bounds.bottom);
if (!event.altKey) {
const others = visibleElements.filter((element) => !drag!.ids.includes(element.id));
const xCandidates = [
0,
500,
1000,
...others.flatMap((element) => [
element.x,
element.x + element.width / 2,
element.x + element.width,
]),
];
const yCandidates = [
0,
325,
650,
...others.flatMap((element) => [
element.y,
element.y + element.height / 2,
element.y + element.height,
]),
];
const xSnap = closestSnap(
[bounds.left + dx, bounds.centerX + dx, bounds.right + dx],
xCandidates
);
const ySnap = closestSnap(
[bounds.top + dy, bounds.centerY + dy, bounds.bottom + dy],
yCandidates
);
if (xSnap) dx += xSnap.delta;
if (ySnap) dy += ySnap.delta;
snapGuides = { x: xSnap?.guide, y: ySnap?.guide };
} else {
snapGuides = {};
}
dx = clamp(dx, -bounds.left, 1000 - bounds.right);
dy = clamp(dy, -bounds.top, 650 - bounds.bottom);
onmove?.(
drag.origins.map((origin) => ({
id: origin.id,
x: Math.round(clamp(origin.x + dx, 0, 1000 - origin.width)),
y: Math.round(clamp(origin.y + dy, 0, 650 - origin.height)),
}))
);
}
function endDrag() {
drag = null;
snapGuides = {};
window.removeEventListener('pointermove', moveDrag);
}
function beginResize(event: PointerEvent, element: ThemeV2Element, direction: ResizeDirection) {
if (!interactive || element.locked) return;
event.preventDefault();
event.stopPropagation();
resize = {
id: element.id,
direction,
startX: event.clientX,
startY: event.clientY,
originX: element.x,
originY: element.y,
originWidth: element.width,
originHeight: element.height,
};
window.addEventListener('pointermove', moveResize);
window.addEventListener('pointerup', endResize, { once: true });
}
function moveResize(event: PointerEvent) {
if (!resize) return;
const dx = (event.clientX - resize.startX) / scale;
const dy = (event.clientY - resize.startY) / scale;
const west = resize.direction.includes('w');
const north = resize.direction.includes('n');
const width = Math.round(clamp(resize.originWidth + (west ? -dx : dx), 24, 1000));
const height = Math.round(clamp(resize.originHeight + (north ? -dy : dy), 24, 650));
onpatch?.(resize.id, {
x: west
? Math.round(clamp(resize.originX + resize.originWidth - width, 0, 1000 - width))
: resize.originX,
y: north
? Math.round(clamp(resize.originY + resize.originHeight - height, 0, 650 - height))
: resize.originY,
width,
height,
});
}
function endResize() {
resize = null;
window.removeEventListener('pointermove', moveResize);
}
function beginMarquee(event: PointerEvent) {
if (!interactive || event.button !== 0 || event.target !== event.currentTarget || !overlay) {
return;
}
const rect = overlay.getBoundingClientRect();
const startX = clamp((event.clientX - rect.left) / scale, 0, 1000);
const startY = clamp((event.clientY - rect.top) / scale, 0, 650);
const additive = modifierPressed(event);
if (!additive) onselectmany?.([], false);
marquee = { startX, startY, x: startX, y: startY, width: 0, height: 0, additive };
window.addEventListener('pointermove', moveMarquee);
window.addEventListener('pointerup', endMarquee, { once: true });
}
function moveMarquee(event: PointerEvent) {
if (!marquee || !overlay) return;
const rect = overlay.getBoundingClientRect();
const currentX = clamp((event.clientX - rect.left) / scale, 0, 1000);
const currentY = clamp((event.clientY - rect.top) / scale, 0, 650);
marquee = {
...marquee,
x: Math.min(marquee.startX, currentX),
y: Math.min(marquee.startY, currentY),
width: Math.abs(currentX - marquee.startX),
height: Math.abs(currentY - marquee.startY),
};
}
function endMarquee() {
if (!marquee) return;
const selection = marquee;
marquee = null;
window.removeEventListener('pointermove', moveMarquee);
if (selection.width < 3 && selection.height < 3) {
return;
}
const hits = visibleElements
.filter(
(element) =>
!element.locked &&
element.x < selection.x + selection.width &&
element.x + element.width > selection.x &&
element.y < selection.y + selection.height &&
element.y + element.height > selection.y
)
.map((element) => element.id);
onselectmany?.(hits, selection.additive);
}
</script>
<div
bind:this={overlay}
class="absolute inset-0 z-20 {interactive ? 'pointer-events-auto' : 'pointer-events-none'}"
role="presentation"
onpointerdown={beginMarquee}
oncontextmenu={(event) => {
if (!interactive || event.target !== event.currentTarget) return;
// The surrounding bits-ui ContextMenu.Trigger opens on the bubbling
// contextmenu event and prevents the native menu itself. Calling
// preventDefault() here makes the trigger treat the event as handled
// (it bails when defaultPrevented is set), leaving no menu at all.
oncontext?.(undefined, event);
}}
>
{#each visibleElements as element (element.id)}
{@const selected = isSelected(element.id)}
{@const singleSelected = selected && selectedIds.length === 1}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div
class="absolute select-none"
class:cursor-move={interactive && !element.locked}
class:cursor-default={interactive && element.locked}
style="left:{element.x}px;top:{element.y}px;width:{element.width}px;height:{element.height}px;transform:rotate({element.rotation ??
0}deg);touch-action:none;outline:{selected
? `${selectedIds.length > 1 ? 1 : 2}px solid #ff006e`
: 'none'};outline-offset:2px;"
role={interactive ? 'button' : undefined}
tabindex={interactive ? 0 : -1}
aria-label={interactive ? `Select ${element.type} layer` : undefined}
onpointerdown={(event) => beginDrag(event, element)}
oncontextmenu={(event) => {
if (!interactive) return;
// Let the event bubble so the editor's ContextMenu.Trigger can open.
oncontext?.(element.id, event);
}}
onkeydown={(event) => {
if (interactive && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
selectElement(element.id, modifierPressed(event));
}
}}
>
<!-- Opacity lives on the content wrapper so selection chrome stays fully opaque. -->
<div class="h-full w-full" style="opacity:{element.opacity ?? 1};">
{#if element.type === 'text'}
<div
class="h-full w-full overflow-hidden"
style="display:flex;align-items:{element.verticalAlign === 'center'
? 'center'
: element.verticalAlign === 'bottom'
? 'flex-end'
: 'flex-start'};justify-content:{element.align === 'center'
? 'center'
: element.align === 'right'
? 'flex-end'
: 'flex-start'};text-align:{element.align ?? 'left'};"
>
<span
style="font-family:{fontStack(element.fontFamily)};font-size:{element.fontSize ??
28}px;font-weight:{element.fontWeight ?? 600};color:{element.color ??
'#ffffff'};line-height:{element.lineHeight ??
1.2};letter-spacing:{element.letterSpacing ??
0}px;white-space:pre-wrap;overflow-wrap:anywhere;"
>
{element.text}
</span>
</div>
{:else if element.type === 'image'}
<img
class="pointer-events-none h-full w-full"
src={assetUrl(element.src)}
alt=""
draggable="false"
style="object-fit:{element.fit ?? 'cover'};border-radius:{element.radius ?? 0}px;"
/>
{:else if element.type === 'video'}
<video
class="pointer-events-none h-full w-full"
use:themeMediaSource={assetUrl(element.src)}
use:themeVideoLifecycle
autoplay
loop
muted={element.muted !== false}
use:applyMuted={element.muted !== false}
playsinline
disablepictureinpicture
aria-hidden="true"
style="object-fit:{element.fit ?? 'cover'};border-radius:{element.radius ?? 0}px;"
></video>
{:else}
<div
class="h-full w-full"
style="background:{element.fill ?? '#ff006e'};border-radius:{element.shape === 'ellipse'
? '50%'
: `${element.radius ?? 16}px`};border:{element.borderWidth
? `${element.borderWidth}px solid ${element.borderColor ?? '#ffffff'}`
: 'none'};"
></div>
{/if}
</div>
{#if singleSelected && !element.locked}
{#each resizeDirections as direction (direction)}
<span
class="absolute z-10 block size-3 rounded-full border-2 border-white bg-primary shadow"
data-resize={direction}
style="{direction.includes('n') ? 'top:-7px' : 'bottom:-7px'};{direction.includes('w')
? 'left:-7px'
: 'right:-7px'};cursor:{direction === 'nw' || direction === 'se'
? 'nwse-resize'
: 'nesw-resize'};"
onpointerdown={(event) => beginResize(event, element, direction)}
role="presentation"
></span>
{/each}
<span
class="absolute -top-7 left-0 rounded bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-primary-foreground shadow"
>
{element.type}
</span>
{/if}
{#if selected && selectedIds.length > 1}
<span
class="absolute -top-5 left-0 rounded bg-primary/90 px-1 py-0.5 text-[8px] font-semibold text-primary-foreground"
>
selected
</span>
{/if}
</div>
{/each}
{#if snapGuides.x !== undefined}
<div
class="pointer-events-none absolute inset-y-0 z-30 w-px bg-primary/80"
style="left:{snapGuides.x}px"
></div>
{/if}
{#if snapGuides.y !== undefined}
<div
class="pointer-events-none absolute inset-x-0 z-30 h-px bg-primary/80"
style="top:{snapGuides.y}px"
></div>
{/if}
{#if marquee}
<div
class="pointer-events-none absolute z-30 border border-primary bg-primary/10"
style="left:{marquee.x}px;top:{marquee.y}px;width:{marquee.width}px;height:{marquee.height}px;"
></div>
{/if}
</div>
@@ -1,13 +0,0 @@
<script lang="ts">
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.ActionProps = $props();
</script>
<AlertDialogPrimitive.Action bind:ref class={cn(buttonVariants(), className)} {...restProps} />
@@ -1,17 +0,0 @@
<script lang="ts">
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.CancelProps = $props();
</script>
<AlertDialogPrimitive.Cancel
bind:ref
class={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
{...restProps}
/>
@@ -1,26 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import AlertDialogOverlay from './alert-dialog-overlay.svelte';
import { AlertDialog as AlertDialogPrimitive, type WithoutChild } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
portalProps,
...restProps
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
portalProps?: AlertDialogPrimitive.PortalProps;
} = $props();
</script>
<AlertDialogPrimitive.Portal {...portalProps}>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
bind:ref
class={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
className
)}
{...restProps}
/>
</AlertDialogPrimitive.Portal>
@@ -1,16 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.DescriptionProps = $props();
</script>
<AlertDialogPrimitive.Description
bind:ref
class={cn('text-muted-foreground text-sm', className)}
{...restProps}
/>
@@ -1,20 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn('flex flex-col space-y-2 text-center sm:text-left', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,19 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.OverlayProps = $props();
</script>
<AlertDialogPrimitive.Overlay
bind:ref
class={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
className
)}
{...restProps}
/>
@@ -1,18 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
level = 3,
...restProps
}: AlertDialogPrimitive.TitleProps = $props();
</script>
<AlertDialogPrimitive.Title
bind:ref
class={cn('text-lg font-semibold', className)}
{level}
{...restProps}
/>
@@ -1,39 +0,0 @@
import Action from './alert-dialog-action.svelte';
import Cancel from './alert-dialog-cancel.svelte';
import Content from './alert-dialog-content.svelte';
import Description from './alert-dialog-description.svelte';
import Footer from './alert-dialog-footer.svelte';
import Header from './alert-dialog-header.svelte';
import Overlay from './alert-dialog-overlay.svelte';
import Title from './alert-dialog-title.svelte';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
const Root = AlertDialogPrimitive.Root;
const Trigger = AlertDialogPrimitive.Trigger;
const Portal = AlertDialogPrimitive.Portal;
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
};
@@ -1,86 +0,0 @@
<script lang="ts">
import { reduceAnimations } from '$lib/userSettings';
//TODO: maybe dynamic background images fetched from ezpp?
const COUNT = 60;
function rand(min: number, max: number) {
return Math.random() * (max - min) + min;
}
const streaks = Array.from({ length: COUNT })
.map((_, i) => {
const depth = rand(0, 2);
const scale = 0.6 + depth * 0.8;
const speed = 0.4 + depth * 1.35;
const distance = 150;
const duration = distance / speed;
return {
width: rand(300, 320) * scale,
height: 32 * scale,
x: (i / COUNT) * 200 - 50,
y: ((i * 137.5) % 100) - 20,
opacity: 0.25 + depth * 0.65,
duration,
delay: rand(0, duration),
scale,
};
})
.sort((a, b) => a.scale - b.scale);
</script>
<div class="absolute bg blur-[2px]">
<div
class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50"
></div>
{#each streaks as s, index (index)}
<div
class="streak drop-shadow-lg transition-opacity duration-1000"
style="
width:{s.width}px;
height:{s.height}px;
left:{s.x}%;
top:{s.y}%;
opacity:{$reduceAnimations ? 0 : s.opacity};
animation-duration:{s.duration}s;
animation-delay:-{s.delay}s;
"
></div>
{/each}
</div>
<style>
.bg {
position: fixed;
inset: 0;
overflow: hidden;
background: linear-gradient(
180deg,
var(--color-primary-300) 0%,
var(--color-primary-600) 40%,
var(--color-primary-900) 100%
);
}
.streak {
position: absolute;
background: #d6f3ff;
border-radius: 999px;
transform: rotate(-35deg);
animation: move linear infinite;
will-change: transform;
}
@keyframes move {
from {
transform: translate(-150vw, 150vh) rotate(-35deg);
}
to {
transform: translate(150vw, -150vh) rotate(-35deg);
}
}
</style>
@@ -1,16 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
class={cn('bg-muted flex h-full w-full items-center justify-center rounded-full', className)}
{...restProps}
/>
@@ -1,16 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
class={cn('aspect-square h-full w-full', className)}
{...restProps}
/>
@@ -1,16 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
class={cn('relative flex size-10 shrink-0 overflow-hidden rounded-full', className)}
{...restProps}
/>
-13
View File
@@ -1,13 +0,0 @@
import Fallback from './avatar-fallback.svelte';
import Image from './avatar-image.svelte';
import Root from './avatar.svelte';
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};
-49
View File
@@ -1,49 +0,0 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({
base: 'focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80 border-transparent',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
</script>
<script lang="ts">
import { cn } from '$lib/utils.js';
import type { WithElementRef } from 'bits-ui';
import type { HTMLAnchorAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
href,
class: className,
variant = 'default',
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? 'a' : 'span'}
bind:this={ref}
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>
-2
View File
@@ -1,2 +0,0 @@
export { default as Badge } from './badge.svelte';
export { badgeVariants, type BadgeVariant } from './badge.svelte';
@@ -1,70 +0,0 @@
<script lang="ts" module>
import type { WithElementRef } from 'bits-ui';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const buttonVariants = tv({
base: 'ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border-input bg-background hover:bg-accent hover:text-accent-foreground border',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from '$lib/utils.js';
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
type = 'button',
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<!-- href is a pass-through prop and may point to an external URL -->
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}
-17
View File
@@ -1,17 +0,0 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from './button.svelte';
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
@@ -1,35 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import Check from '@lucide/svelte/icons/check';
import Minus from '@lucide/svelte/icons/minus';
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
class={cn(
'border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content size-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50',
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div class="flex size-4 items-center justify-center text-current">
{#if indeterminate}
<Minus class="size-3.5" />
{:else}
<Check class={cn('size-3.5', !checked && 'text-transparent')} />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
-7
View File
@@ -1,7 +0,0 @@
import Root from './checkbox.svelte';
export {
Root,
//
Root as Checkbox,
};
@@ -1,43 +0,0 @@
<script lang="ts">
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import CheckIcon from '@lucide/svelte/icons/check';
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
inset,
children: childrenProp,
closeOnSelect = false,
...restProps
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
inset?: boolean;
children?: Snippet;
} = $props();
</script>
<ContextMenuPrimitive.CheckboxItem
bind:ref
{closeOnSelect}
bind:checked
bind:indeterminate
data-slot="context-menu-checkbox-item"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="absolute right-2 pointer-events-none">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</ContextMenuPrimitive.CheckboxItem>
Loaded 100 of 188 files, more files were not shown because too many files have changed in this diff. Show more