diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..c1cd848
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,29 @@
+{
+ "env": {
+ "browser": true,
+ "commonjs": true,
+ "es2021": true
+ },
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": 13
+ },
+ "rules": {
+ "no-undef": 0,
+ "no-unused-vars": 0,
+ "no-case-declarations": 0,
+
+ "indent": [
+ "error",
+ 2
+ ],
+ "quotes": [
+ "error",
+ "single"
+ ],
+ "semi": [
+ "error",
+ "never"
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 2ccb550..66207a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ bin/
dist/
ext/
temp/
+gc*/
resources/js/neutralino.js
resources/bg/official
diff --git a/README.md b/README.md
index 9bb046c..f662b2c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
# GrassClipper
-Grasscutter launcher for easily switching between Official and Private servers
+Experimental Grasscutter launcher for easily switching between Official and Private servers
-[Download Here!](https://github.com/Grasscutters/GrassClipper/releases/)
+[Download Here!](https://github.com/Grasscutters/GrassClipper/releases/) (Supports Windows 8+)
+
+*\*Note: some translations are outdated, so if random English text appears or an option seems misleading, this is why. If you notice an issue like this, feel free to make a pull request!*
# Table of Contents
@@ -9,60 +11,132 @@ Grasscutter launcher for easily switching between Official and Private servers
* [Setup (for development)](#setup-for-development)
* [TODO](#todo)
* [Common Problems](#having-problems)
+ * [Proxy Installation not Opening/Failing](#manual-proxy-installation)
* [White Screen Fix](#white-screen-fix)
+ * [Error 502](#error-502)
+ * [Error 4206](#error-4206)
+ * [Infinite CMD Windows](#infinite-cmd-windows)
* [Broken Discord/Youtube](#my-discord-is-not-letting-me-send-messages-or-load-images-my-youtube-is-acting-strange)
* [No internet](#i-have-no-internet-after-closing-everything-restarting-my-pc)
+* [Languages and Translation Credits](#available-languages-and-translation-credits)
* [Screenshots](#screenshots)
# Setup (for Users)
1. Download the zip file
2. Extract the zip file somewhere
-3. Run `GrassClipper.exe`, install the proxy server, and set your `Genshin Impact Game` folder!
+3. Run `GrassClipper.exe`, install the proxy server, and set your game folder!
# Setup (for Development)
0. Clone the repository
1. Ensure you have [NodeJS](https://nodejs.org/en/download/) installed.
2. Install the `neu` CLI tool: `npm install -g @neutralinojs/neu`
-3. Install the dependencies: `npm install` AND `neu update`
+3. Install the dependencies: `setup_win.cmd`
4. Compile and run:
* For testing: `npm run dev`
* For production: `npm run build`
# TODO
-* Interface
+* Interface/internals
* [x] UI
* [x] Official and Private options
* [x] Server IP input
* [x] Fun fancy CSS styling n stuff (CoD: MW 2019-style vertical menu for choosing between official and private servers? [See this](https://charlieintel.com/wp-content/uploads/2020/11/MW-new-menu.png))
- * [ ] Custom images for private server sections (anyone is welcome to submit a pull request to add some!)
- * [ ] Optional username/password creation for servers before entering (not implemented in GrassCutter yet)
* [x] Kill switch script (optional)
* [x] Automatically run `install.cmd` when opening for the first time
- * [ ] Fix Windows scaling issues?
+ * [x] Grasscutter auto-downloader
+ * [ ] Detect when in a folder that is inaccessible to the program (eg. `C:/Program Files`) and warn
+ * [ ] Custom images for private server sections (anyone is welcome to submit a pull request to add some!)
+ * [x] Optional username/password creation for servers before entering (not implemented in Grasscutter yet)
+ * [ ] Platform detection and bash scripts
+ * [ ] Integrated banner creator
* Proxy service
* [x] Local proxy server
* [x] Intercept and modify GI requests like with Fiddler, allow anything else to pass through
- * [ ] Fix Discord and YouTube issues when proxy is enabled (not sure what's up with them?)
+ * [ ] Fix Discord and YouTube issues when proxy is enabled (maybe fixed)
# Having problems?
Below are some scenarios you may encounter and their solutions.
-### White Screen Fix
+# Manual Proxy Installation
-Encountering a white screen? [Ensure WebView2 is installed](https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/#download)
+If you having trouble installing the proxy server, you can also install it manually. To do so:
+1. Create a folder if it does not exist named `ext` in the GrassClipper folder.
+2. Download and extract the contents of [this file](https://snapshots.mitmproxy.org/7.0.4/mitmproxy-7.0.4-windows.zip) into the `ext` folder
+3. Double click `mitmdump.exe` and allow it to run for a few seconds to generate the certificate
+4. Run this command as Administrator: `certutil -addstore root "%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer"`
+5. Use GrassClipper like normal!
-### My Discord is not letting me send messages or load images/My Youtube is acting strange!
+## White Screen Fix
+
+Encountering a white screen? [Ensure WebView2 is installed](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download)
+
+You may also want to run this command as administrator:
+`CheckNetIsolation.exe LoopbackExempt -a -n="Microsoft.Win32WebViewHost_cw5n1h2txyewy"`
+
+If you have Chinese characters in your file path, this may crash it too! I am working on a fix.
+
+You can also try running in Windows 8 compatitbility mode.
+
+If all else fails, you can run GrassClipper in `chrome` or `browser` mode. To do so:
+* Create a shortcut to `GrassClipper.exe`
+* Right click the shortcut, click `properties`
+* In the `Target` box, at the very end, add ` --mode=chrome` or ` --mode=browser`
+ * `chrome` only works if you have Chrome installed, and will create a Chrome window
+ * `brower` will, you guessed it, open GrassClipper in your default browser
+* Click `Ok`
+* Run GrassClipper using this shortcut from now on!
+
+## Error 502
+
+1. If you are running a local server, ensure the local server is running. Otherwise, ensure the server you are connecting to is actually running.
+
+2. If you are able, [use the development branch of Grasscutter](https://github.com/Grasscutters/Grasscutter/tree/development). It is known to work better with GrassClipper.
+
+If you are still getting an error 502 when attempting to log in to your own server, open your Grasscutter config and add the following to the `DispatchServer` section:
+
+```json
+"PublicPort": YOUR_PORT
+```
+where `YOUR_PORT` is the same port you use as the `Port` value already. This will probably be 443.
+
+## Error 4206
+
+Ensure you have the correct `keystore.p12` file that comes with your branch (`stable` or `development`). Also ensure the password is set properly in Grasscutters `config.json` (blank for `stable`, "123456" for `development`).
+
+## Infinite CMD Windows
+
+If you are getting infinite CMD windows for any of the scripts (such as the proxy installation, or private server start), ensure you have UAC (user access control) set to any option that requires asking. Ensure your user account can open things as Admin.
+
+## My Discord is not letting me send messages or load images/My Youtube is acting strange!
Discord/YouTube (plus surely some others) does not seem to be a fan of the proxy server. You may need to disable it by either closing mitmdump or by disabling your proxy in the Windows proxy settings.
-### I have no internet after closing everything/restarting my PC!
+## I have no internet after closing everything/restarting my PC!
The launcher most likely did not close correctly, and was unable to clean your proxy settings back to what they were. Disable your proxy in the Windows proxy settings.
+# Available Languages and Translation Credits
+
+Thank you to everyone who has provided translations! <3
+
+* ZH - nuoxianCN, Scirese & MrAru
+* ZH-TW - Kimi & KormiMeiko
+* PT-BR - na.na
+* VIE - labalityowo & lunaticwhat
+* ID - Iqrar99 & nautilust
+* FR - linsorak & memetrollsXD
+* ES - memetrollsXD
+* ND - memetrollsXD
+* RU - fitiskin
+* TR - lilmayofuksu
+* JP - conochy
+* HD - Arikatsu
+* PL - zakhil-dev
+
# Screenshots
![image](https://user-images.githubusercontent.com/25207995/164574276-645548c2-7ba6-47c3-8df4-77082003648f.png)
@@ -70,6 +144,7 @@ The launcher most likely did not close correctly, and was unable to clean your p
![image](https://user-images.githubusercontent.com/25207995/164393040-4da72f29-6d59-4af4-bd60-072269f2ba2a.png)
![image](https://user-images.githubusercontent.com/25207995/164393024-56543ddf-7063-4c04-9a9f-0c6238f30e90.png)
![image](https://user-images.githubusercontent.com/25207995/164393118-de844e75-f9a2-491a-aea6-f2d563abecc7.png)
-![image](https://user-images.githubusercontent.com/25207995/164574339-50bbda5e-e25a-47c3-ae14-2b0b0b4ca92c.png)
+![image](https://user-images.githubusercontent.com/25207995/164882735-77aa535c-0e93-4b32-af7c-f8b59888257a.png)
+![image](https://user-images.githubusercontent.com/25207995/164882716-c9f16cd0-c0b6-4c0a-ae9e-4c95da9ef7f5.png)
diff --git a/README_PL.md b/README_PL.md
new file mode 100644
index 0000000..61cd6dc
--- /dev/null
+++ b/README_PL.md
@@ -0,0 +1,150 @@
+# GrassClipper
+Experymentalny launcher Grasscutter'a stworzony dla łatwego przełączania się pomiędzy serwerami oficjalnymi, a prywatnymi
+
+[Pobierz tutaj!](https://github.com/Grasscutters/GrassClipper/releases/) (Wspiera Windows'a 8+)
+
+*\*Uwaga: niektóre tłumaczenia są nie aktualne, więc jeżeli widzisz tekst po angielsku, lub jakiś tekst wprowadza w błąd, to to jest powodem. W przypadku zauważenia takiego błędu, śmiało otwórz pull request'a!*
+
+# Spis treści
+
+* [Instalacja (dla użytkownika)](#instalacja-dla-użytkownika)
+* [Instalacja (dla developera)](#instalacja-dla-developera)
+* [Do zrobienia](#do-zrobienia)
+* [Częste problmy](#napotkałeś-problem?)
+ * [Instalacja proxy nie uruchamia się / nie powodzi się](#ręczna-instalacja-proxy)
+ * [Naprawa białego ekranu](#naprawa-białego-ekranu)
+ * [Błąd 502](#błąd-502)
+ * [Błąd 4206](#błąd-4206)
+ * [Nieskończone okna CMD](#nieskończone-okna-CMD)
+ * [Niedziałający Discord/Youtube](#discord-nie-pozwala-wysyłać-mi-wiadomości-lub-wczytywać-obrazków/YouTube-dziwnie-się-zachowuje)
+ * [Brak internetu](#nie-mam-dostępu-do-internetu-po-zamknięciu-wszystkiego/restarcie-komputera!)
+* [Dostępne tłumaczenia oraz ich autorzy](#dostępne-tłumaczenia-oraz-ich-autorzy)
+* [Zrzuty ekranu](#zrzuty-ekranu)
+
+# Instalacja (dla użytkownika)
+
+1. Pobierz plik zip
+2. Wypakuj gdzieś plik zip
+3. Uruchom `GrassClipper.exe`, zainstaluj serwer proxy oraz ustaw folder z grą!
+
+# Instalacja (dla developera)
+
+0. Sklonuj repozytorium
+1. Upewnij się, że masz zainstalowanego [NodeJS'a](https://nodejs.org/en/download/).
+2. Zainstaluj narzędzie `neu` CLI: `npm install -g @neutralinojs/neu`
+3. Zainstaluj zależności: `setup_win.cmd`
+4. Skompiluj i uruchom:
+ * Testowanie: `npm run dev`
+ * Produkcja: `npm run build`
+
+# Do zrobienia
+
+* Interfejst/wewnętrzne
+ * [x] UI
+ * [x] Opcje Oficjalny i Prywatny
+ * [x] Wprowadzanie IP serwera
+ * [x] Stylowanie CSS - Pionowe menu do wybierania pomiędzy oficjalnym, a prywatnym serwerem? (Stylizowane na CoD: MW 2019) [Zobacz tutaj](https://charlieintel.com/wp-content/uploads/2020/11/MW-new-menu.png))
+ * [x] Skrypt Kill Switch (opcjonalne)
+ * [x] Automatyczne uruchamianie `install.cmd`, jeżeli uruchamamy program po raz pierwszy
+ * [x] Auto-pobieranie Grasscutter'a
+ * [ ] Wykrywanie gdy program jest uruchomiony w folderze, do którego nie ma dostępu (np. `C:/Program Files`) i wysyłanie ostrzeżenia.
+ * [ ] Nowe obrazki dla sekcji serwera prywatnego (Pull request'y od wszystkich są mile widziane!)
+ * [x] Opcjonalne tworzenie loginu/hasła dla serwera przed połączeniem (nie zaimplementowane jeszcze w Grasscutter)
+ * [ ] Wykrywanie systemu i skrypty bash
+ * [ ] Zintegrowany kreator bannerów
+* Usługa proxy
+ * [x] Lokalny serwer proxy
+ * [x] Przechwytywanie i modyfikowanie zapytań GI, np. przy użyciu Fiddler'a, przepuszczanie wszystkigo innego.
+ * [ ] Naprawienie problemów z Discordem i YouTube'm gdy proxy jest włączone (może naprawione)
+
+# Napotkałeś problem?
+
+Poniżej znajdują się najczęściej pojawiające się problemy oraz ich rozwiązania.
+
+## Ręczna instalacja proxy
+
+Jeżeli napotkałeś problemy z automatyczną instalacją proxy, możesz zainstalować je ręcznie.
+W tym celu:
+1. Jeżeli nie istnieje stwórz folder `ext` w folderze GrassClipper'a.
+2. Pobierz i wypakuj zawartość [tego pliku](https://snapshots.mitmproxy.org/7.0.4/mitmproxy-7.0.4-windows.zip) do folderu `ext`
+3. Uruchom `mitmdump.exe` i poczekaj kilka chwil, żeby wygenerował certyfikat.
+4. Wywołaj to polecenie jako Administrator: `certutil -addstore root "%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer"`
+5. GrassClipper powinien działać poprawnie!
+
+## Naprawa białego ekranu
+
+Napotkałeś biały ekran? Upewnij się czy [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download) jest zainstalowany.
+
+Wywołanie tego polecenia jako Administrator również może pomóc:
+`CheckNetIsolation.exe LoopbackExempt -a -n="Microsoft.Win32WebViewHost_cw5n1h2txyewy"`
+
+Jeżeli w twojej ścieżce do pliku występują chińskie symbole, to mogą one być tego przyczyną. Pracujemy nad poprawką.
+
+Możesz spróbować uruchomić aplikacje w trybie zgodności z Windows'em 8
+
+Jeżeli nic z tego nie pomoże, możesz uruchomić GrassClipper'a w trybie `chrome` lub `browser`. W tym celu:
+* Stwórz skrót do `GrassClipper.exe`
+* Kliknij na niego prawym przyciskiem, wybierz `Właściwości`
+* W polu `Element docelowy`, na samym końcu, dodaj ` --mode=chrome` lub ` --mode=browser`
+ * `chrome` działa tylko wtedy gdy masz zainstalowanego Chrome'a, otworzy okienko Chrome'a
+ * `brower` otworzy GrassClippera w twojej domyślnej przeglądarce
+* Kliknij `Ok`
+* Od teraz uruchamiaj GrassClippera nowo stworzonym skrótem.
+
+## Błąd 502
+
+1. Jeżeli dołączas na serwer lokalny to upewnij się czy jest włączony. W przeciwnym wypadku, upewnij się, czy serwer do którego się łączysz działa.
+
+2. Jeżeli to możliwe, [użyj wersji developerskiej Grasscutter'a](https://github.com/Grasscutters/Grasscutter/tree/development). Z tego co wiadomo to działa lepiej z GrassClipper'em.
+
+Jeżeli dalej dostajesz błąd 502 przy próbie logowania do własnego serwera, otwórz plik `config.json` w folderze Grasscutter'a i dodaj w nim w sekcji `DispatchServer`:
+
+```json
+"PublicPort": TWÓJ_PORT
+```
+
+`TWÓJ_PORT` jest tym samym portem co w polu `Port`. Jest to prawdopodobnie wartość 443.
+
+## Błąd 4206
+
+Upewnij się, że posiadasz odpowiedni plik `keystore.p12` dla twojej wersji. Sprawdź, czy hasło jest poprawnie ustawione w pliku `config.json` (puste dla wersji `stabilnej`, "123456" dla wersji `developerskiej`).
+
+## Nieskończone okna CMD
+
+Jeżeli jakikolwiek skrypt uruchamia nieskończoność okien CMD (np. instalator proxy albo starter serwera prywatnego), upewnij się, że UAC (User Access Control) jest ustawiony na dowolną opcje która wymaga potwierdzenia. Sprawdź czy twoje konto może uruchamiać programy jako Administrator.
+
+## Discord nie pozwala wysyłać mi wiadomości lub wczytywać obrazków/YouTube dziwnie się zachowuje
+
+Discord/Youtube (i na pewno pare innych) nie przepadają za serwerem proxy. Musisz go wyłączyć zamykając mitmdump, albo wyłączając proxy w ustawieniach Windowsa.
+
+## Nie mam dostępu do internetu po zamknięciu wszystkiego/restarcie komputera!
+
+Launcher najprawdopodobniej nie został poprawnie zamknięty, przez co nie był w stanie przywrócić oryginalnych ustawień proxy. Wyłącz proxy w ustawieniach Windows'a.
+
+# Dostępne tłumaczenia oraz ich autorzy
+
+Dziękujemy dla wszystkich, którzy pomagają w tłumaczeniu! <3
+
+* ZH - nuoxianCN, Scirese & MrAru
+* ZH-TW - Kimi & KormiMeiko
+* PT-BR - na.na
+* VIE - labalityowo
+* ID - Iqrar99 & nautilust
+* FR - linsorak & memetrollsXD
+* ES - memetrollsXD
+* ND - memetrollsXD
+* RU - fitiskin
+* TR - lilmayofuksu
+* JP - conochy
+* HD - Arikatsu
+* PL - zakhil-dev
+
+# Zrzuty ekranu
+
+![image](https://user-images.githubusercontent.com/25207995/164574276-645548c2-7ba6-47c3-8df4-77082003648f.png)
+![image](https://user-images.githubusercontent.com/25207995/164393190-f7e6633c-60bd-4186-bf0c-30d9f30871f4.png)
+![image](https://user-images.githubusercontent.com/25207995/164393040-4da72f29-6d59-4af4-bd60-072269f2ba2a.png)
+![image](https://user-images.githubusercontent.com/25207995/164393024-56543ddf-7063-4c04-9a9f-0c6238f30e90.png)
+![image](https://user-images.githubusercontent.com/25207995/164393118-de844e75-f9a2-491a-aea6-f2d563abecc7.png)
+![image](https://user-images.githubusercontent.com/25207995/164882735-77aa535c-0e93-4b32-af7c-f8b59888257a.png)
+![image](https://user-images.githubusercontent.com/25207995/164882716-c9f16cd0-c0b6-4c0a-ae9e-4c95da9ef7f5.png)
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..8f73f6a
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,23 @@
+# !/bin/bash
+
+# Clean dist
+rm -rf ./dist
+
+# build
+neu build
+
+# copy scripts and langs
+cp -r ./scripts ./dist/GrassClipper
+cp -r ./proxy ./dist/GrassClipper
+cp -r ./languages ./dist/GrassClipper
+
+# copy backgrounds
+mkdir ./dist/GrassClipper/resources/
+mkdir ./dist/GrassClipper/resources/bg
+mkdir ./dist/GrassClipper/resources/bg/private
+mkdir ./dist/GrassClipper/resources/bg/server
+cp -r ./resources/bg/private/* ./dist/GrassClipper/resources/bg/private
+cp -r ./resources/bg/server/* ./dist/GrassClipper/resources/bg/server
+
+# rename exe
+mv ./dist/GrassClipper/GrassClipper-win_x64.exe ./dist/GrassClipper/GrassClipper.exe
\ No newline at end of file
diff --git a/build_win.cmd b/build_win.cmd
new file mode 100644
index 0000000..d0f18d7
--- /dev/null
+++ b/build_win.cmd
@@ -0,0 +1,32 @@
+@echo off
+
+:: This binary is the SSL-secure release
+set SSL_BINARY_URL="https://github.com/SpikeHD/neutralinojs/releases/download/v420.69.0/neutralino-win_x64.exe"
+set NON_SSL_BINARY_URL="https://github.com/SpikeHD/neutralinojs/releases/download/v1337.0.0/neutralino-win_x64.exe"
+
+:: Clean dist folder
+del /s /q /f .\dist>nul
+rd /s /q .\dist
+
+:: Get the SSL-secure version of the binary
+powershell Invoke-WebRequest -Uri %SSL_BINARY_URL% -OutFile "./bin/neutralino-win_x64.exe"
+
+:: build
+call neu build
+
+:: Copy scripts and langs
+xcopy .\languages\ .\dist\GrassClipper\languages\ /y /s
+xcopy .\proxy\ .\dist\GrassClipper\proxy\ /y /s
+xcopy .\scripts\ .\dist\GrassClipper\scripts\ /y /s
+
+:: bgs
+mkdir .\dist\GrassClipper\resources\bg\private
+mkdir .\dist\GrassClipper\resources\bg\server
+xcopy .\resources\bg\private\ .\dist\GrassClipper\resources\bg\private\ /y /s
+xcopy .\resources\bg\server\ .\dist\GrassClipper\resources\bg\server\ /y /s
+
+:: rename exe
+move .\dist\GrassClipper\GrassClipper-win_x64.exe .\dist\GrassClipper\GrassClipper.exe
+
+:: Re-use non-ssl secure version
+powershell Invoke-WebRequest -Uri %NON_SSL_BINARY_URL% -OutFile "./bin/neutralino-win_x64.exe"
\ No newline at end of file
diff --git a/languages/en.json b/languages/en.json
index 9140483..d7ba60a 100644
--- a/languages/en.json
+++ b/languages/en.json
@@ -3,14 +3,15 @@
"appName": "GrassClipper",
"playOfficial": "Play Official",
- "playPrivate": "Play Private",
+ "playPrivate": "Play on Grasscutter",
"launchLocalServer": "Launch Local Server",
- "genshinFolderSet": "Set \"Genshin Impact Game\" folder",
- "grasscutterFileSet": "Set \"GrassCutter\" .jar file",
+ "gameExeSet": "Set game executable",
+ "grasscutterFileSet": "Set \"Grasscutter\" .jar file",
"folderNotSet": "Not set",
- "ipPlaceholder": "IP Address",
+ "ipPlaceholder": "Server Address...",
+ "portPlaceholder": "Port",
"noFavorites": "No favorites set",
"settingsTitle": "Settings",
@@ -20,20 +21,71 @@
"proxyOption": "Proxy",
"proxySubtitle": "Install the proxy server via the install script",
"updateOption": "Update",
- "updateSubtitle": "Auto updating is temporarily disabled. Check GitHub for the newest releease.",
+ "updateSubtitle": "Auto updating is temporarily disabled. Check GitHub for the newest release.",
"languageOption": "Language",
"languageSubtitle": "Select your language!",
"enableServerLauncherOption": "Enable Server Launcher",
"enableServerLauncherSubtitle": "Enable to server launcher tile for launching a local Grasscutter instance.",
+ "httpsOption": "Use HTTPS",
+ "httpsSubtitle": "Choose between using HTTPS or HTTP.",
"introSen1": "Looks like this is your first time opening GrassClipper!",
"introSen2": "First of all, welcome, happy to see you here! :)",
"introSen3": "Would you like to run the proxy installer?",
- "introSen4": "(required to connect to private servers)",
+ "introSen4": "(required to connect to servers)",
+ "updateBtn": "Update",
"proxyInstallBtn": "Install",
"proxyInstallDeny": "No thanks",
- "genshinFolderDialog": "Select Genshin Impact Game folder",
- "grasscutterFileDialog": "Select GrassCutter server jar file"
-}
\ No newline at end of file
+ "gameFolderDialog": "Select game folder",
+ "grasscutterFileDialog": "Select Grasscutter server jar file",
+
+ "loggingInTo": "Logging in to: ",
+ "registeringFor": "Registering for: ",
+ "authUsername": "Username: ",
+ "authPassword": "Password: ",
+ "authConfirmPassword": "Confirm Password: ",
+ "authLoginBtn": "Login",
+ "authRegisterBtn": "Register",
+ "authLoginTitle": "Login",
+ "authRegisterTitle": "Register",
+ "launchWithoutAuth": "Launch without Authentication",
+
+ "alertInvalid": "Invalid username or password",
+ "alertNoPass": "No password set, please change password",
+ "alertUnknown": "Unknown error, contact server owner",
+ "alertAuthNoLogin": "Authentication is disabled, no need to log in!",
+ "alertLoginSuccess": "Login successful! Token copied to clipboard. Paste this token into the username field of the game to log in.",
+
+ "alertUserTaken": "Username is taken",
+ "alertPassMismatch": "Password and password confirmation do not match",
+ "alertAuthNoRegister": "Authentication is disabled, no need to register!",
+ "alertRegisterSuccess": "Registration successful!",
+
+ "downloadTitle": "Downloads",
+ "grassclipperTitle": "GrassClipper",
+ "grasscutterTitle": "Grasscutter",
+ "installerTitle": "Installer",
+ "installerSubtitle": "Installs proxy and other tools. Required for Grasscutter servers.",
+ "downloadStable": "Download Grasscutter Stable Build",
+ "stableSubtitle": "Install Grasscutter stable branch. This build usually has less bugs, but also less features.",
+ "downloadDev": "Download Grasscutter Development Build",
+ "devSubtitle": "Install Grasscutter development branch. This build sometimes has bugs, and is frequently updated. Use at your own risk.",
+ "downloadResources": "Download Grasscutter Resources",
+ "resourceSubtitle": "Downloads Grasscutter resources into the currently set Grasscutter folder. This should be done unless you plan on getting resources externally.",
+
+ "gcScriptRunning": "Running...",
+ "stableInstall": "Download",
+ "devInstall": "Download",
+
+ "updateNotifText": "A new update is available! Newest version: ",
+
+ "chineseCharacterAlert": "The file path set contains Chinese characters, this may cause problems!",
+
+ "dialogOk": "OK",
+ "dialogNo": "NO",
+ "serverEnableDialogTitle": "You found the Grasscutter server launcher!",
+ "serverEnableDialogText": "If you do not have an existing Grasscutter installation to set, would you like to download a build?"
+
+}
diff --git a/languages/es.json b/languages/es.json
new file mode 100644
index 0000000..9fce461
--- /dev/null
+++ b/languages/es.json
@@ -0,0 +1,39 @@
+{
+ "fullLangName": "Español (México)",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Jugar en oficial",
+ "playPrivate": "Jugar en Grasscutter",
+ "launchLocalServer": "Iniciar servidor local",
+
+ "gameExeSet": "Establece el folder game",
+ "grasscutterFileSet": "Establece el archivo .jar de \"Grasscutter\"",
+ "folderNotSet": "No establecido",
+
+ "ipPlaceholder": "Dirección IP",
+ "noFavorites": "No hay favoritos",
+
+ "settingsTitle": "Ajustes",
+ "scriptsSectionTitle": "Scripts",
+ "killswitchOption": "Kill Switch",
+ "killswitchSubtitle": "Solo para aquellos muy paranoicos con bans. Cierra el proceso del juego *y tu Internet* si algo le sucede al proxy.",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "Instale el servidor proxy a través del script de instalación",
+ "updateOption": "Update",
+ "updateSubtitle": "Las actualizaciónes automáticas están deshabilitadas temporalmente. Consulte GitHub para ver la versión más reciente.",
+ "languageOption": "Idioma",
+ "languageSubtitle": "Seleccióne su idioma!",
+ "enableServerLauncherOption": "Habilita el launcher del servidor",
+ "enableServerLauncherSubtitle": "Habilite el tile del launche del servidor para iniciar una instancia local de Grasscutter.",
+
+ "introSen1": "¡Parece que es la primera vez que abres GrassClipper!",
+ "introSen2": "En primer lugar, ¡bienvenido, feliz de verte por aquí! :)",
+ "introSen3": "Quieres ejecutar el instalador de proxy?",
+ "introSen4": "(necesario para conectarse a los servidores)",
+
+ "proxyInstallBtn": "Instalar",
+ "proxyInstallDeny": "No gracias",
+
+ "gameFolderDialog": "Selecciona la carpeta game",
+ "grasscutterFileDialog": "Selecciona el archivo .jar de Grasscutter"
+}
diff --git a/languages/fr.json b/languages/fr.json
new file mode 100644
index 0000000..485d401
--- /dev/null
+++ b/languages/fr.json
@@ -0,0 +1,39 @@
+{
+ "fullLangName": "Français",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Jouer sur Officiel",
+ "playPrivate": "Jouer sur Grasscutter",
+ "launchLocalServer": "Lancer le serveur en local",
+
+ "gameExeSet": "Définir le dossier game",
+ "grasscutterFileSet": "Définir le fichier .jar \"Grasscutter\"",
+ "folderNotSet": "Pas encore défini",
+
+ "ipPlaceholder": "Adresse IP",
+ "noFavorites": "Aucun favori n'est défini",
+
+ "settingsTitle": "Paramètres",
+ "scriptsSectionTitle": "Scripts",
+ "killswitchOption": "Coupe-Circuit",
+ "killswitchSubtitle": "Seulement pour ceux qui sont très paranoïaques au sujet des bannissements. Tuez le processus de jeu *et coupez votre Internet* si le proxy ne fonctionne pas correctement.",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "Install the proxy server via the install script",
+ "updateOption": "Mise à jour",
+ "updateSubtitle": "La mise à jour automatique est temporairement désactivée. Consultez GitHub pour la dernière version.",
+ "languageOption": "Langages",
+ "languageSubtitle": "Sélectionnez votre langue!",
+ "enableServerLauncherOption": "Activer le lanceur de serveur",
+ "enableServerLauncherSubtitle": "Autoriser le lanceur à executer une instance locale de Grasscutter",
+
+ "introSen1": "On dirait que c’est la première fois que vous ouvrez GrassClipper!",
+ "introSen2": "Bienvenue! Nous sommes heureux de vous voir parmis nous :)",
+ "introSen3": "Voulez-vous lancer l’installateur du proxy?",
+ "introSen4": "(nécessaire pour se connecter sur des serveurs Grasscutter)",
+
+ "proxyInstallBtn": "Installer",
+ "proxyInstallDeny": "Non merci",
+
+ "gameFolderDialog": "Sélectionnez le dossier contenant le game",
+ "grasscutterFileDialog": "Selectionnez le fichier jar du serveur de Grasscutter"
+}
diff --git a/languages/hd.json b/languages/hd.json
new file mode 100644
index 0000000..dbad1b8
--- /dev/null
+++ b/languages/hd.json
@@ -0,0 +1,85 @@
+{
+ "fullLangName": "Hindi",
+ "appName": "ग्रासक्लिपर",
+
+ "playOfficial": "खेलो असली गेम",
+ "playPrivate": "खेलो ग्रासकटर पर",
+ "launchLocalServer": "शुरू करे लोकल सर्वर",
+
+ "gameExeSet": "गेम का निष्पादन करे",
+ "grasscutterFileSet": "लगाए \"ग्रासकटर \" .jar फ़ाइल",
+ "folderNotSet": "नहीं लगा है",
+
+ "ipPlaceholder": "आय पी एड्रेस",
+ "portPlaceholder": "पोर्ट",
+ "noFavorites": "कोई पसन्दीदार नहीं लगे",
+
+ "settingsTitle": "सेटिंग्स",
+ "scriptsSectionTitle": "स्क्रिप्ट्स",
+ "killswitchOption": "किल स्विच",
+ "killswitchSubtitle": "सिर्फ उनके लिए जो सोचते हैं की यह सब असली अकाउंट को बन कर देग। यह गेम और *इंटरनेट कनेक्शन* मर देता है अगर कुछ प्रॉक्सी को होता है। ",
+ "proxyOption": "प्रॉक्सी",
+ "proxySubtitle": "प्रॉक्सी सर्वर इनस्टॉल करे प्रॉक्सी स्क्रिप्ट्स से",
+ "updateOption": "अपडेट ",
+ "updateSubtitle": "अपने आप अपडेट होने का फीचर अभी बंद है। GitHub देखे नए रिलीज़ के लिए। ",
+ "languageOption": "भाषा ",
+ "languageSubtitle": "अपनी भाषा चुने!",
+ "enableServerLauncherOption": "सर्वर लांचर शुरू करे",
+ "enableServerLauncherSubtitle": "सक्षम करे सर्वर लांचर टाइल ताकि ये एक लोकल ग्रासकटर इंस्टैंस को शुरू कर सके। ",
+ "httpsOption": "HTTPS का उपयोग करे",
+ "httpsSubtitle": "चयन करे HTTPS या HTTP",
+
+ "introSen1": "लग रहा है यह तुम्हारा पहली बार खोलना हुआ है ग्रास्सक्लीप्पेर!",
+ "introSen2": "पहली बात, स्वागत है, अच्छा लगा आपको देख के यहाँ! :)",
+ "introSen3": "प्रॉक्सी इंस्टालर शुरू करे?",
+ "introSen4": "(ज़रुरत है इसकी सर्वर से कनेक्ट करने के लिए)",
+
+ "updateBtn": "अपडेट ",
+ "proxyInstallBtn": "इनस्टॉल ",
+ "proxyInstallDeny": "कोई ज़रुरत नहीं",
+
+ "gameFolderDialog": "गेम फोल्डर सेट करे",
+ "grasscutterFileDialog": "ग्रासकटर jar फाइल सेलेक्ट करे",
+
+ "loggingInTo": "लोग हो रहे हैं इसमें: ",
+ "registeringFor": "रजिस्टर करे इसके लिए: ",
+ "authUsername": "यूजरनाम: ",
+ "authPassword": "पासवर्ड: ",
+ "authConfirmPassword": "पासवर्ड पक्का करे: ",
+ "authLoginBtn": "लॉगिन",
+ "authRegisterBtn": "रजिस्टर",
+ "authLoginTitle": "लॉगिन",
+ "authRegisterTitle": "रजिस्टर",
+ "launchWithoutAuth": "शुरू करे बिना ऑथेंटिकेशन के",
+
+ "alertInvalid": "गलत यूजरनाम या पासवर्ड",
+ "alertNoPass": "कोई पासवर्ड नहीं लगा, कृपया पासवर्ड चेंज करे",
+ "alertUnknown": "अनजान एरर। सर्वर के मालिक से बात करे",
+ "alertAuthNoLogin": "ऑथेंटिकेशन बंद है, लॉगिन की कोई ज़रुरत नहीं!",
+ "alertLoginSuccess": "लॉगिन पूरा हुआ! टोकन क्लिपबोर्ड पर कॉपी हो गया है। यह टोकन पेस्ट करे यूजरनाम की जगह पर गेम के अंदर ताकि लॉगिन हो सके। ",
+
+ "alertUserTaken": "यूजरनाम ले लिया गया है",
+ "alertPassMismatch": "पासवर्ड और पासवर्ड कन्फर्मेशन मैच नहीं कर रहे",
+ "alertAuthNoRegister": "ऑथेंटिकेशन बंद है, रजिस्ट्रेशन की कोई ज़रुरत नहीं!",
+ "alertRegisterSuccess": "रजिस्ट्रेशन समाप्त हुआ!",
+
+ "downloadTitle": "डौन्लोडस",
+ "grassclipperTitle": "ग्रासक्लिपर",
+ "grasscutterTitle": "ग्रासकटर",
+ "installerTitle": "इंस्टालर",
+ "installerSubtitle": "ये प्रॉक्सी और कुछ और टूल्स इनस्टॉल करता है। ज़रुरत है इसकी ग्रासकटर सर्वर्स के लिए",
+ "downloadStable": "डाउनलोड ग्रासकटर स्टेबल बिल्ड",
+ "stableSubtitle": "इनस्टॉल ग्रासकटर स्टेबल ब्रांच। यह बिल्ड में काम फीचर्स है पर काम बग्स भी है।",
+ "downloadDev": "डाउनलोड ग्रासकटर डेवलपमेंट बिल्ड",
+ "devSubtitle": "इनस्टॉल ग्रासकटर डेवलपमेंट ब्रांच। इस बिल्ड में काफी बार बग्स होते हैं पर जल्दी जल्दी अपडेट होता है। अपने बूते पे इस्तेमाल करे।",
+ "downloadResources": "डाउनलोड ग्रसससटर रिसोर्सेज",
+ "resourceSubtitle": "आपभी सेट हुए ग्रासकटर फोल्डर में ग्रासकटर के रिसोर्सेज डाउनलोड करता है। यह हो जाना चाहिए अगर आपने कही और से रिसोर्सेज नहीं लिये।",
+
+ "gcScriptRunning": "चल रहा है...",
+ "stableInstall": "डाउनलोड",
+ "devInstall": "डाउनलोड",
+
+ "updateNotifText": "नया अपडेट आया है! नया वर्शन: ",
+
+ "chineseCharacterAlert": "इस फाइल पथ में चीनी चरक्टेर्स हैं, यह समस्या दे सकता है!"
+}
diff --git a/languages/id.json b/languages/id.json
new file mode 100644
index 0000000..2685f12
--- /dev/null
+++ b/languages/id.json
@@ -0,0 +1,90 @@
+{
+ "fullLangName": "Bahasa Indonesia",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Mainkan Official",
+ "playPrivate": "Mainkan di Grasscutter",
+ "launchLocalServer": "Luncurkan Server Lokal",
+
+ "gameExeSet": "Atur eksekusi game",
+ "grasscutterFileSet": "Atur file \"Grasscutter\" .jar",
+ "folderNotSet": "Belum diatur",
+
+ "ipPlaceholder": "Alamat IP",
+ "portPlaceholder": "Port",
+ "noFavorites": "Tidak ada favorit yang diatur",
+
+ "settingsTitle": "Pengaturan",
+ "scriptsSectionTitle": "Skrip",
+ "killswitchOption": "Saklar Mati",
+ "killswitchSubtitle": "Hanya untuk yang paranoid terhadap ban. Mematikan proses game *dan internetmu* jika terjadi sesuatu pada proxy nya.",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "Pasang server proxy melalui skrip pemasangan",
+ "updateOption": "Pembaruan",
+ "updateSubtitle": "Pembaruan otomatis dinonaktifkan untuk sementara. Cek GitHub untuk rilisan terbaru.",
+ "languageOption": "Bahasa",
+ "languageSubtitle": "Pilih bahasamu!",
+ "enableServerLauncherOption": "Aktifkan Peluncur Server",
+ "enableServerLauncherSubtitle": "Aktifkan ke tile peluncur server untuk meluncurkan instance Grasscutter secara lokal.",
+ "httpsOption": "Gunakan HTTPS",
+ "httpsSubtitle": "Pilih antara menggunakan HTTPS atau HTTP.",
+
+ "introSen1": "Sepertinya ini pertama kalinya kamu membuka GrassClipper!",
+ "introSen2": "Pertama-tama, selamat datang, senang bertemu denganmu disini! :)",
+ "introSen3": "Apakah kamu ingin menjalankan pemasangan proxy?",
+ "introSen4": "(diperlukan untuk menghubungkan ke server)",
+
+ "updateBtn": "Perbarui",
+ "proxyInstallBtn": "Pasang",
+ "proxyInstallDeny": "Tidak, terima kasih",
+
+ "gameFolderDialog": "Pilih folder game",
+ "grasscutterFileDialog": "Pilih file jar server Grasscutter",
+
+ "loggingInTo": "Masuk ke: ",
+ "registeringFor": "Mendaftar untuk: ",
+ "authUsername": "Nama Pengguna: ",
+ "authPassword": "Kata Sandi: ",
+ "authConfirmPassword": "Konfirmasi Kata Sandi: ",
+ "authLoginBtn": "Masuk",
+ "authRegisterBtn": "Daftar",
+ "authLoginTitle": "Masuk",
+ "authRegisterTitle": "Daftar",
+ "launchWithoutAuth": "Luncurkan Tanpa Autentikasi",
+
+ "alertInvalid": "Nama pengguna atau kata sandi tidak valid",
+ "alertNoPass": "Tidak ada kata sandi yang diatur, silahkan isi kata sandi",
+ "alertUnknown": "Kesalahan yang tidak diketahui, hubungi pemilik server",
+ "alertAuthNoLogin": "Autentikasi dinonaktifkan, tidak perlu masuk!",
+ "alertLoginSuccess": "Berhasil masuk! Token disalin ke papan klip. Tempelkan token ini ke dalam bidang nama pengguna di game untuk masuk.",
+
+ "alertUserTaken": "Nama pengguna sudah terpakai",
+ "alertPassMismatch": "Kata sandi dan konfirmasi kata sandi tidak cocok",
+ "alertAuthNoRegister": "Autentikasi dinonaktifkan, tidak perlu mendaftar!",
+ "alertRegisterSuccess": "Pendaftaran berhasil!",
+
+ "downloadTitle": "Unduhan",
+ "grassclipperTitle": "GrassClipper",
+ "grasscutterTitle": "Grasscutter",
+ "installerTitle": "Pemasang",
+ "installerSubtitle": "Pasang proxy dan alat lainnya. Diperlukan untuk server Grasscutter.",
+ "downloadStable": "Unduh Versi Stabil Grasscutter",
+ "stableSubtitle": "Pasang versi stabil Grasscutter. Versi ini biasanya tidak punya banyak bug, tetapi juga fiturnya lebih sedikit.",
+ "downloadDev": "Unduh Versi Pengembangan Grasscutter",
+ "devSubtitle": "Pasang versi pengembangan Grasscutter. Versi ini biasanya mempunyai banyak bug dan sering diperbarui. Gunakan dengan resikomu sendiri.",
+ "downloadResources": "Unduh Sumber Daya Grasscutter",
+ "resourceSubtitle": "Unduh sumber daya Grasscutter ke dalam folder Grasscutter yang sudah diatur. Ini harus dilakukan kecuali kamu berencana untuk mendapatkan sumber daya secara eksternal.",
+
+ "gcScriptRunning": "Menjalankan...",
+ "stableInstall": "Unduh",
+ "devInstall": "Unduh",
+
+ "updateNotifText": "Pembaruan baru tersedia! Versi terbaru: ",
+
+ "chineseCharacterAlert": "Jalur file yang diatur berisi karakter Cina, ini dapat menyebabkan masalah!",
+
+ "dialogOk": "OK",
+ "dialogNo": "TIDAK",
+ "serverEnableDialogTitle": "Kamu menemukan peluncur server Grasscutter!",
+ "serverEnableDialogText": "Jika kamu tidak memiliki pemasangan Grasscutter untuk diatur, apakah kamu mau mengunduh build nya?"
+}
diff --git a/languages/jp.json b/languages/jp.json
new file mode 100644
index 0000000..4951772
--- /dev/null
+++ b/languages/jp.json
@@ -0,0 +1,85 @@
+{
+ "fullLangName": "日本語",
+ "appName": "GrassClipper",
+
+ "playOfficial": "起動 (Official)",
+ "playPrivate": "起動 (Grasscutter)",
+ "launchLocalServer": "ローカルサーバーを起動",
+
+ "gameExeSet": "GenshinImpact.exe の場所を指定",
+ "grasscutterFileSet": "grasscutter.jar の場所を指定",
+ "folderNotSet": "原神のフォルダーが指定されていません。",
+
+ "ipPlaceholder": "IP アドレス",
+ "portPlaceholder": "ポート",
+ "noFavorites": "お気に入りはまだ登録されていません",
+
+ "settingsTitle": "設定",
+ "scriptsSectionTitle": "スクリプト",
+ "killswitchOption": "強制終了",
+ "killswitchSubtitle": "緊急時のみ。プロキシにエラーが発生した場合に、ゲームを終了し、インターネットを遮断します。",
+ "proxyOption": "プロキシ",
+ "proxySubtitle": "プロキシサーバーをインストールします。",
+ "updateOption": "更新",
+ "updateSubtitle": "自動更新は現在無効化されています。GitHub で最新バージョンを確認してください。",
+ "languageOption": "言語",
+ "languageSubtitle": "言語を選択してください。",
+ "enableServerLauncherOption": "サーバーランチャーを有効化します",
+ "enableServerLauncherSubtitle": "Grasscutter を起動するための、ランチャータイルを有効化します。",
+ "httpsOption": "HTTPS を使用",
+ "httpsSubtitle": "HTTP と HTTPS のどちらを使用するか選択します。このオプションは、プライベートサーバーでプレイするときにのみ適用されます。",
+
+ "introSen1": "GrassClipper へようこそ!",
+ "introSen2": "GrassClipper をご利用いただきありがとうございます。",
+ "introSen3": "プロキシサーバーをインストールしますか?",
+ "introSen4": "(インターネットへの接続が必要)",
+
+ "updateBtn": "更新",
+ "proxyInstallBtn": "インストールする",
+ "proxyInstallDeny": "インストールしない",
+
+ "gameFolderDialog": "原神のゲームフォルダーを指定",
+ "grasscutterFileDialog": "grasscutter.jar の場所を指定",
+
+ "loggingInTo": "ログイン先: ",
+ "registeringFor": "登録先: ",
+ "authUsername": "ユーザー名: ",
+ "authPassword": "パスワード: ",
+ "authConfirmPassword": "パスワード(確認): ",
+ "authLoginBtn": "ログイン",
+ "authRegisterBtn": "登録",
+ "authLoginTitle": "ログイン",
+ "authRegisterTitle": "登録",
+ "launchWithoutAuth": "認証せず起動",
+
+ "alertInvalid": "ユーザー名またはパスワードが誤っています。",
+ "alertNoPass": "パスワードが設定されていません。パスワードを設定してください。",
+ "alertUnknown": "不明なエラーが発生しました。サーバーの管理者に連絡してください。",
+ "alertAuthNoLogin": "認証が無効になっています。ログインする必要はありません。",
+ "alertLoginSuccess": "ログインに成功しました。トークンをクリップボードにコピーしたので、ゲームのユーザー名欄に貼り付けてログインしてください。",
+
+ "alertUserTaken": "そのユーザー名はすでに使用されています。",
+ "alertPassMismatch": "パスワードが一致していません。",
+ "alertAuthNoRegister": "認証が無効になっています。ログインする必要はありません。",
+ "alertRegisterSuccess": "登録成功",
+
+ "downloadTitle": "ダウンロード",
+ "grassclipperTitle": "GrassClipper",
+ "grasscutterTitle": "Grasscutter",
+ "installerTitle": "インストーラー",
+ "installerSubtitle": "Grasscutter に必要な、プロキシなどのツールをインストールします。",
+ "downloadStable": "Grasscutter の安定版をインストール",
+ "stableSubtitle": "Grasscutter の安定版をインストールします。バグは少ないですが、新機能も少ないです。",
+ "downloadDev": "Grasscutter の開発版をインストール",
+ "devSubtitle": "Grasscutter の開発版をインストールします。バグが多い恐れがあります。また、頻繁に更新されます。",
+ "downloadResources": "Grasscutter のリソースをダウンロード",
+ "resourceSubtitle": "Grasscutter のリソースをダウンロードします。ほかのところから入手しない場合は、これをダウンロードする必要があります。",
+
+ "gcScriptRunning": "実行中...",
+ "stableInstall": "ダウンロード",
+ "devInstall": "ダウンロード",
+
+ "updateNotifText": "新しいバージョンが利用できます。Version: ",
+
+ "chineseCharacterAlert": "ファイルパスに漢字やひらがななどの全角文字が含まれているため、正常に動作しない場合があります。"
+}
diff --git a/languages/nl.json b/languages/nl.json
new file mode 100644
index 0000000..03cdb3b
--- /dev/null
+++ b/languages/nl.json
@@ -0,0 +1,39 @@
+{
+ "fullLangName": "Nederlands",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Normaal spelen",
+ "playPrivate": "Op Grasscutter spelen",
+ "launchLocalServer": "Lokale server starten",
+
+ "gameExeSet": "Selecteer de game folder",
+ "grasscutterFileSet": "Selecteer het \"Grasscutter\" .jar bestand",
+ "folderNotSet": "Niet geselecteerd",
+
+ "ipPlaceholder": "IP adres",
+ "noFavorites": "Geen favorieten",
+
+ "settingsTitle": "Instellingen",
+ "scriptsSectionTitle": "Scripts",
+ "killswitchOption": "Kill Switch",
+ "killswitchSubtitle": "Alleen voor de paranoïde gebruikers. Als iets met de proxy gebeurt, sluit dit jouw spel *en je internet* af.",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "Installeer de proxy server via de installeer script",
+ "updateOption": "Update",
+ "updateSubtitle": "Automatisch updaten is tijdelijk uitgeschakeld. Ga naar de GitHub voor de nieuwste release.",
+ "languageOption": "Taal",
+ "languageSubtitle": "Selecteer je taal!",
+ "enableServerLauncherOption": "Server Launcher inschakelen",
+ "enableServerLauncherSubtitle": "Schakel dit in om de server launcher tile te gebruiken om een lokale Grasscutter server te starten.",
+
+ "introSen1": "Het lijkt erop dat het de eerste keer is dat je Grasscutter gebruikt!",
+ "introSen2": "Ten eerste, welkom, goed om jou hier te zien! :)",
+ "introSen3": "Wil je de proxy installer uitvoeren?",
+ "introSen4": "(vereist om te verbinden naar servers)",
+
+ "proxyInstallBtn": "Installeren",
+ "proxyInstallDeny": "Nee dank je",
+
+ "gameFolderDialog": "Selecteer de game folder",
+ "grasscutterFileDialog": "Selecteer het Grasscutter server jar bestand"
+}
diff --git a/languages/pl.json b/languages/pl.json
new file mode 100644
index 0000000..0056f92
--- /dev/null
+++ b/languages/pl.json
@@ -0,0 +1,91 @@
+{
+ "fullLangName": "Polski",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Serwer oficjalny",
+ "playPrivate": "Serwer prywatny",
+ "launchLocalServer": "Uruchom serwer prywatny",
+
+ "gameExeSet": "Ustaw plik gry",
+ "grasscutterFileSet": "Ustaw plik \"Grasscutter\" .jar ",
+ "folderNotSet": "Nie ustawiono",
+
+ "ipPlaceholder": "Adres serwera",
+ "portPlaceholder": "Port",
+ "noFavorites": "Nie dodano żadnych ulubionych",
+
+ "settingsTitle": "Ustawienia",
+ "scriptsSectionTitle": "Skrypty",
+ "killswitchOption": "Kill Switch",
+ "killswitchSubtitle": "Tylko dla tych, którzy boją się o dostanie bana. Wyłącza gre *oraz twój internet* jeżeli coś stanie się z proxy.",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "Zainstaluj serwer proxy poprzez skrypt instalacyjny",
+ "updateOption": "Aktualizacja",
+ "updateSubtitle": "Auto-aktualizacje są niedostępne. Sprawdź GitHub'a projektu po nowe wersje.",
+ "languageOption": "Język",
+ "languageSubtitle": "Wybierz swój język!",
+ "enableServerLauncherOption": "Włącz Launcher Serwera",
+ "enableServerLauncherSubtitle": "Enable to server launcher tile for launching a local Grasscutter instance.",
+ "httpsOption": "Używaj HTTPS",
+ "httpsSubtitle": "Wybierz pomiędzy używaniem HTTPS, a HTTP.",
+
+ "introSen1": "Wygląda na to, że po raz pierwszy uruchomiłeś GrassClipper'a!",
+ "introSen2": "Po pierwsze, miło Cię tu widzieć! :)",
+ "introSen3": "Czy chcesz uruchomić instalator proxy?",
+ "introSen4": "(proxy wymagane jest do łączenia się z serwerem prywatnym)",
+
+ "updateBtn": "Aktualizuj",
+ "proxyInstallBtn": "Instaluj",
+ "proxyInstallDeny": "Nie, dziękuję",
+
+ "gameFolderDialog": "Ustaw folder gry",
+ "grasscutterFileDialog": "Wybierz plik serwera Grasscutter",
+
+ "loggingInTo": "Logowanie do: ",
+ "registeringFor": "Rejestracja do: ",
+ "authUsername": "Nazwa użytkownika: ",
+ "authPassword": "Hasło: ",
+ "authConfirmPassword": "Potwierdź hasło: ",
+ "authLoginBtn": "Zaloguj",
+ "authRegisterBtn": "Zarejestruj",
+ "authLoginTitle": "Logowanie",
+ "authRegisterTitle": "Rejestracja",
+ "launchWithoutAuth": "Uruchom bez autentykacji",
+
+ "alertInvalid": "Błędna nazwa użytkownika lub hasło",
+ "alertNoPass": "Nie ustawiono hasła, proszę ustawić hasło",
+ "alertUnknown": "Nieznany błąd, skontaktuj się z właścicielem serwera",
+ "alertAuthNoLogin": "Autentykacja jest wyłączona, nie ma potrzeby logowania się!",
+ "alertLoginSuccess": "Pomyślnie zalogowano! Twój Token został skopiowany do schowka. Wklej go w polu nazwy użytkownika w grze aby się zalogować.",
+
+ "alertUserTaken": "Ta nazwa jest już zajęta",
+ "alertPassMismatch": "Hasła nie zgadzają się",
+ "alertAuthNoRegister": "Autentykacja jest wyłączona, nie ma potrzeby rejestrowania się!",
+ "alertRegisterSuccess": "Pomyślnie zarejestrowano!",
+
+ "downloadTitle": "Pobieranie",
+ "grassclipperTitle": "GrassClipper",
+ "grasscutterTitle": "Grasscutter",
+ "installerTitle": "Instalator",
+ "installerSubtitle": "Instaluje proxy oraz inne niezbędne narzędzia do grania na serwerach Grasscutter'a.",
+ "downloadStable": "Pobierz stabilną wersję Grasscutter'a",
+ "stableSubtitle": "Zainstaluj stabilną wersję Grasscutter'a. Ma ona mało błędów, ale też mniej funkcji.",
+ "downloadDev": "Pobierz wersję developerską Grasscutter'a",
+ "devSubtitle": "Zainstaluj wersję developerską Grasscutter'a. Może posiadać sporo błędów, ma najnowsze funkcje, używaj na własne ryzyko.",
+ "downloadResources": "Pobierz Zasoby Grasscutter'a",
+ "resourceSubtitle": "Pobiera zasoby Grasscuterra do aktualnie wybranego folderu. Nie powinno się tego uruchamiać gdy zasoby chcesz pobierać z zewnątrz.",
+
+ "gcScriptRunning": "Pracuje...",
+ "stableInstall": "Pobierz wersje stabilną",
+ "devInstall": "Pobierz wersje deweloperską",
+
+ "updateNotifText": "Nowa wersja jest dostępna! Najnowsza wersja: ",
+
+ "chineseCharacterAlert": "Ścieżka pliku zawierająca chińskie symbole może powodować błędy.",
+
+ "dialogOk": "Tak",
+ "dialogNo": "Nie",
+ "serverEnableDialogTitle": "Znalazłeś Launcher Serwera",
+ "serverEnableDialogText": "Jeżeli nie posiadasz instalacji Grasscutter'a, możesz ją teraz pobrać. Uruchomić pobieranie?"
+
+}
diff --git a/languages/ru.json b/languages/ru.json
new file mode 100644
index 0000000..86c42c3
--- /dev/null
+++ b/languages/ru.json
@@ -0,0 +1,40 @@
+{
+ "fullLangName": "Русский",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Официальная игра",
+ "playPrivate": "Grasscutter",
+ "launchLocalServer": "Локальный сервер",
+
+ "gameExeSet": "Папка с игрой",
+ "grasscutterFileSet": "Путь до файла \"Grasscutter\" (.jar)",
+ "folderNotSet": "Папка не выбрана",
+
+ "ipPlaceholder": "IP-адрес",
+ "portPlaceholder": "Порт (опционально)",
+ "noFavorites": "В избранном пусто",
+
+ "settingsTitle": "Настройки",
+ "scriptsSectionTitle": "Скрипты",
+ "killswitchOption": "Аварийное прерывание",
+ "killswitchSubtitle": "Опция для тех, кто очень беспокоится по поводу банов. Снимает задачу с игрой *и разрывает ваше интернет-соединение*, когда возникают неполадки с прокси",
+ "proxyOption": "Прокси",
+ "proxySubtitle": "Запустить установочный скрипт для настройки прокси-сервера",
+ "updateOption": "Обновление",
+ "updateSubtitle": "Автоматическое обновление временно отключено. Вы можете проверить наличие обновлений на GitHub",
+ "languageOption": "Язык",
+ "languageSubtitle": "Выберите предпочитаемый язык приложения",
+ "enableServerLauncherOption": "Включить запуск сервера",
+ "enableServerLauncherSubtitle": "Использовать лаунчер для локального запуска Grasscutter",
+
+ "introSen1": "Кажется, вы впервые запустили GrassClipper!",
+ "introSen2": "Добро пожаловать, мы рады вас видеть! :)",
+ "introSen3": "Запустить установщик прокси?",
+ "introSen4": "(необходим для подключения к приватным серверам)",
+
+ "proxyInstallBtn": "Установить",
+ "proxyInstallDeny": "Нет, спасибо",
+
+ "gameFolderDialog": "Выбор папки с игрой",
+ "grasscutterFileDialog": "Выбор пути до файла Grasscutter (.jar)"
+}
diff --git a/languages/tr.json b/languages/tr.json
new file mode 100644
index 0000000..7f8a68d
--- /dev/null
+++ b/languages/tr.json
@@ -0,0 +1,43 @@
+{
+ "fullLangName": "Türkçe",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Resmi sunucuda oyna",
+ "playPrivate": "Grasscutter sunucusunda oyna",
+ "launchLocalServer": "Yerel sunucuyu başlat",
+
+ "gameExeSet": "Oyun çalıştırıcısını seç",
+ "grasscutterFileSet": "\"Grasscutter\" .jar dosyasını seç",
+ "folderNotSet": "Seçilmedi",
+
+ "ipPlaceholder": "IP Adresi",
+ "portPlaceholder": "Port",
+ "noFavorites": "Favori sunucunuz yok.",
+
+ "settingsTitle": "Ayarlar",
+ "scriptsSectionTitle": "Scriptler",
+ "killswitchOption": "Kill Switch",
+ "killswitchSubtitle": "Sadece banlanmaktan paranoyak olanlar için. Proxy'ye bir şey olduğunda oyunu kapatıp *internet bağlantınızı* keser.",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "Proxy sunucusunu bir script ile kur.",
+ "updateOption": "Güncelle",
+ "updateSubtitle": "Otomatik güncellemeler geçici olarak devre dışıdır. Yeni sürümler için GitHub'ı kontrol edin.",
+ "languageOption": "Dil",
+ "languageSubtitle": "Dilinizi seçin!",
+ "enableServerLauncherOption": "Sunucu başlatıcısını etkinleştir",
+ "enableServerLauncherSubtitle": "Yerel Grasscutter sunucusunu başlatmak için kutucuğunu etkinleştirin.",
+ "httpsOption": "HTTPS kullan",
+ "httpsSubtitle": "HTTPS veya HTTP arasında geçiş yap.",
+
+ "introSen1": "Görünüşe göre GrassClipper'ı ilk kez açıyorsunuz!",
+ "introSen2": "İlk olarak, hoşgeldiniz. Sizi burada görmek bizi mutlu ediyor! :)",
+ "introSen3": "Proxy kurucusunu çalıştırmak ister misiniz?",
+ "introSen4": "(sunuculara bağlanmak için gerekli)",
+
+ "updateBtn": "Güncelle",
+ "proxyInstallBtn": "Kur",
+ "proxyInstallDeny": "Hayır, teşekkürler.",
+
+ "gameFolderDialog": "Oyun klasörünü seç",
+ "grasscutterFileDialog": "Grasscutter.jar dosyasını seç"
+}
diff --git a/languages/vie.json b/languages/vie.json
new file mode 100644
index 0000000..9626c7f
--- /dev/null
+++ b/languages/vie.json
@@ -0,0 +1,90 @@
+{
+ "fullLangName": "Tiếng Việt",
+ "appName": "GrassClipper",
+
+ "playOfficial": "Máy chủ chính thức",
+ "playPrivate": "Máy chủ riêng",
+ "launchLocalServer": "Khởi động máy chủ cục bộ",
+
+ "gameExeSet": "Chọn tệp \"GenshinImpact.exe\"",
+ "grasscutterFileSet": "Chọn tệp \"Grasscutter.jar\"",
+ "folderNotSet": "Chưa chọn tệp",
+
+ "ipPlaceholder": "Địa chỉ máy chủ",
+ "portPlaceholder": "Cổng",
+ "noFavorites": "Không có máy chủ yêu thích",
+
+ "settingsTitle": "Cài đặt",
+ "scriptsSectionTitle": "Scripts",
+ "killswitchOption": "Thoát khẩn cấp",
+ "killswitchSubtitle": "Để phòng tránh bị ban, thoát game *và ngắt internet* của bạn ngay lập tức nếu có vấn đề xảy ra.",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "Dùng để kết nối vào máy chủ riêng",
+ "updateOption": "Cập nhật",
+ "updateSubtitle": "Tính năng tự động cập nhập tạm thời chưa khả dụng. Kiểm tra Github để biết thêm chi tiết",
+ "languageOption": "Ngôn ngữ",
+ "languageSubtitle": "Thay đổi ngôn ngữ",
+ "enableServerLauncherOption": "Grasscutter",
+ "enableServerLauncherSubtitle": "Thêm lựa chọn bật máy chủ grasscutter",
+ "httpsOption": "Dùng HTTPS",
+ "httpsSubtitle": "Thay đổi giao thức giữa HTTPS và HTTP",
+
+ "introSen1": "Hình như đây là lần đầu tiên bạn dùng ứng dụng này!",
+ "introSen2": "Trước tiên xin chào và cảm ơn bạn đã sử dụng! :)",
+ "introSen3": "Bạn có muốn cài đặt ngay proxy không?",
+ "introSen4": "(cần thiết để vào máy chủ riêng)",
+
+ "updateBtn": "Cập nhật",
+ "proxyInstallBtn": "Cài đặt",
+ "proxyInstallDeny": "Huỷ bỏ",
+
+ "gameFolderDialog": "Chọn tệp GenshinImpact.exe",
+ "grasscutterFileDialog": "Chọn tệp Grasscutter.jar",
+ "loggingInTo": "Đang đăng nhập với tên: ",
+ "registeringFor": "Đang đăng ký tài khoản: ",
+ "authUsername": "Tài khoản: ",
+ "authPassword": "Mật khẩu: ",
+ "authConfirmPassword": "Xác nhận lại mật khẩu: ",
+ "authLoginBtn": "Đăng nhập",
+ "authRegisterBtn": "Đăng ký",
+ "authLoginTitle": "Đăng nhập",
+ "authRegisterTitle": "Đăng ký",
+ "launchWithoutAuth": "Khởi chạy không cần hệ thống xác thực",
+
+ "alertInvalid": "Tài khoản hoặc mật khẩu không đúng",
+ "alertNoPass": "Chưa có mật khẩu. Vui lòng thiết lập mật khẩu",
+ "alertUnknown": "Lỗi không xác định, báo cáo với người quản lý máy chủ",
+ "alertAuthNoLogin": "Đã vô hiệu hoá hệ thống xác thực, không cần đăng nhập.",
+ "alertLoginSuccess": "Đăng nhập thành công! Token đã được sao chép vào bộ nhớ tạm. Dán token vào phần tài khoản trong game để đăng nhập.",
+
+ "alertUserTaken": "Tên tài khoản đã tồn tại",
+ "alertPassMismatch": "Mật khẩu xác thực không trùng khớp",
+ "alertAuthNoRegister": "Đã vô hiệu hoá hệ thống xác thực, không cần đăng ký.",
+ "alertRegisterSuccess": "Đăng ký thành công!",
+
+ "downloadTitle": "Tải về",
+ "grassclipperTitle": "GrassClipper",
+ "grasscutterTitle": "Grasscutter",
+ "installerTitle": "Phần cài đặt",
+ "installerSubtitle": "Cài đặt proxy và những phần khác. Cần thiết cho những máy chủ Grasscutter.",
+ "downloadStable": "Tải về Grasscutter phiên bản Stable",
+ "stableSubtitle": "Cài đặt Grasscutter phiên bản Stable. Đây là phiên bản ổn định, tuy nhiên sẽ có ít tính năng.",
+ "downloadDev": "Tải về Grasscutter phiên bản Development",
+ "devSubtitle": "Cài đặt Grasscutter phiên bản Development. Đây là phiên bản mới hơn, thường xuyên cập nhập hơn nhưng có thể sẽ xuất hiện lõi trong quá trình sử dụng.",
+ "downloadResources": "Tài về tài nguyên cho Grasscutter",
+ "resourceSubtitle": "Tải về phần tài nguyên cần thiết cho Grasscutter. Sử dụng tính năng này để tài về phần tài nguyên tự động nếu bạn không muốn tải về thủ công.",
+
+ "gcScriptRunning": "Đang chạy...",
+ "stableInstall": "Tải về",
+ "devInstall": "Tải về",
+
+ "updateNotifText": "Phiên bản mới nhất đang khả dụng: ",
+
+ "chineseCharacterAlert": "Đường dẫn tới tệp tin chứa ký tự phức tạp, có thể gây lỗi!",
+
+ "dialogOk": "OK",
+ "dialogNo": "Không",
+ "serverEnableDialogTitle": "Đã tìm thấy trình khởi chạy Grasscutter!",
+ "serverEnableDialogText": "Không tìm thấy phần cài đặt Grasscutter hiện tại, bạn có muốn tải về?"
+
+}
diff --git a/languages/zh-tw.json b/languages/zh-tw.json
new file mode 100644
index 0000000..bb0f316
--- /dev/null
+++ b/languages/zh-tw.json
@@ -0,0 +1,91 @@
+{
+ "fullLangName": "繁體中文",
+ "appName": "GrassClipper啟動器",
+
+ "playOfficial": "啟動官方伺服器",
+ "playPrivate": "啟動私人伺服器",
+ "launchLocalServer": "啟動本地伺服器",
+
+ "gameExeSet": "設定\"GenshinImpact\".exe執行檔",
+ "grasscutterFileSet": "設定 \"Grasscutter\".jar 檔案",
+ "folderNotSet": "尚未設定Genshin Impact game資料夾",
+
+ "ipPlaceholder": "IP地址",
+ "portPlaceholder": "端口",
+ "noFavorites": "没有收藏",
+
+ "settingsTitle": "設定",
+ "scriptsSectionTitle": "程式設定",
+ "killswitchOption": "終止服務進程",
+ "killswitchSubtitle": "這是給對於封號非常敏感的人們,啟用此選項將會在你Proxy中發生任何錯誤時會終止你的遊戲(還有你的網路。)",
+ "proxyOption": "Proxy",
+ "proxySubtitle": "通過批次檔來安裝Proxy伺服器。",
+ "updateOption": "更新",
+ "updateSubtitle": "自動更新暫時無法使用。查看GitHub以獲取最新的Release。",
+ "languageOption": "語言",
+ "languageSubtitle": "選擇你最熟悉的語言!",
+ "enableServerLauncherOption": "啟用本地伺服器",
+ "enableServerLauncherSubtitle": "啟動器內將會為你顯示啟動本地伺服器選項。",
+ "httpsOption": "使用 HTTPS",
+ "httpsSubtitle": "選擇使用HTTP或HTTPS,此選項適用於私人伺服器。",
+
+ "introSen1": "看來這是你第一次使用GrassClipper啟動器!",
+ "introSen2": "首先,歡迎你使用GrassClipper啟動器,其次很高興在這裡見到你! :)",
+ "introSen3": "是否要運行Proxy安裝程序?",
+ "introSen4": "(需要連接到私人伺服器)",
+
+ "updateBtn": "更新",
+ "proxyInstallBtn": "安裝",
+ "proxyInstallDeny": "不用了,謝謝。",
+
+ "gameFolderDialog": "選擇Genshin Impact game資料夾",
+ "grasscutterFileDialog": "選擇Grasscutter.jar檔案",
+
+ "loggingInTo": "登錄至:",
+ "registeringFor": "註冊至:",
+ "authUsername": "用戶名: ",
+ "authPassword": "密碼: ",
+ "authConfirmPassword": "再次確認密碼: ",
+ "authLoginBtn": "登錄",
+ "authRegisterBtn": "註冊",
+ "authLoginTitle": "登錄",
+ "authRegisterTitle": "註冊",
+ "launchWithoutAuth": "在未認證的情況下啟動",
+
+ "alertInvalid": "用戶名或密碼不正確。",
+ "alertNoPass": "未設定密碼,請先更改密碼",
+ "alertUnknown": "未知錯誤,請聯系伺服器管理員",
+ "alertAuthNoLogin": "未啟用認證,無需登錄!",
+ "alertLoginSuccess": "登錄成功!Token 已複製至剪貼簿。將 Token 複製到用戶名處即可登錄。",
+
+ "alertUserTaken": "用戶名已被占用",
+ "alertPassMismatch": "兩組密碼不一致",
+ "alertAuthNoRegister": "未啟用認證,無需註冊!",
+ "alertRegisterSuccess": "註冊成功!",
+
+ "downloadTitle": "安裝清單",
+ "grassclipperTitle": "GrassClipper",
+ "grasscutterTitle": "Grasscutter",
+ "installerTitle": "安裝程式",
+ "installerSubtitle": "安裝Proxy和其他工具。 Grasscutter伺服器連接時需要。",
+ "downloadStable": "安裝 Stable 構建",
+ "stableSubtitle": "安裝 Stable 分支. 此構建通常具有較少的錯誤,但功能也很少。",
+ "downloadDev": "安裝 Development 構建",
+ "devSubtitle": "安裝 Development 分支. 此版本有時存在錯誤,並且經常更新。 使用此版本風險自負。",
+ "downloadResources": "下載 resources 資料夾",
+ "resourceSubtitle": "將 resources 安裝在目前設置的 Grasscutter 資料夾中。 除非您計劃從外部獲取resources,否則應該這樣做。",
+
+ "gcScriptRunning": "執行中...",
+ "stableInstall": "安裝",
+ "devInstall": "安装",
+
+ "updateNotifText": "有新的GrassClipper更新可用! 最新版本: ",
+
+ "chineseCharacterAlert": "此路徑含有中文字體,這可能會導致問題的發生。",
+
+ "dialogOk": "好的",
+ "dialogNo": "不要",
+ "serverEnableDialogTitle": "您找到了Grasscutter本地伺服器啟動器!",
+ "serverEnableDialogText": "如果你現在還沒有把Grasscutter安裝在電腦上面的話,要不要直接從網路上下載?"
+
+}
diff --git a/languages/zh.json b/languages/zh.json
new file mode 100644
index 0000000..0202466
--- /dev/null
+++ b/languages/zh.json
@@ -0,0 +1,84 @@
+{
+ "fullLangName": "简体中文",
+ "appName": "GrassClipper 启动器",
+
+ "playOfficial": "启动官方服务器",
+ "playPrivate": "启动私人服务器",
+ "launchLocalServer": "启动本地服务器",
+
+ "gameExeSet": "选择游戏的可执行文件",
+ "grasscutterFileSet": "选择 \"Grasscutter\" .jar 文件",
+ "folderNotSet": "没有选择游戏可执行文件或 Grasscutter.jar 文件",
+
+ "ipPlaceholder": "服务器 IP 地址",
+ "portPlaceholder": "端口",
+ "noFavorites": "没有收藏任何 IP 地址",
+
+ "settingsTitle": "设置",
+ "scriptsSectionTitle": "脚本",
+ "killswitchOption": "杀死服务进程",
+ "killswitchSubtitle": "为那些害怕被封号的人准备。启用此选项可以结束游戏进程(还可以让你断网,如果修改了代理设置的话)。",
+ "proxyOption": "代理",
+ "proxySubtitle": "通过批处理文件 (install.cmd) 以安装代理服务器。",
+ "updateOption": "更新",
+ "updateSubtitle": "自动更新暂时不可用。查看 GitHub 以获取最新的版本。",
+ "languageOption": "语言",
+ "languageSubtitle": "选择你熟悉的语言以应用 GrassClipper 启动器!",
+ "enableServerLauncherOption": "启用本地服务器",
+ "enableServerLauncherSubtitle": "在启动器内显示启动本地服务器选项。",
+ "httpsOption": "使用 HTTPS",
+ "httpsSubtitle": "选择是否使用 HTTP(S)",
+
+ "introSen1": "看来这是你第一次使用 GrassClipper 启动器!",
+ "introSen2": "首先,欢迎你使用 GrassClipper 启动器,其次很高兴见到你! :)",
+ "introSen3": "是否要运行安装脚本来安装代理服务器?",
+ "introSen4": "(此选项仅提供给需要连接到私人服务器的用户)",
+ "updateBtn": "更新",
+ "proxyInstallBtn": "安装",
+ "proxyInstallDeny": "不用了,谢谢",
+
+ "gameFolderDialog": "选择游戏文件夹",
+ "grasscutterFileDialog": "选择 Grasscutter.jar 文件",
+
+ "loggingInTo": "登录至:",
+ "registeringFor": "注册至:",
+ "authUsername": "用户名: ",
+ "authPassword": "密码: ",
+ "authConfirmPassword": "再次确认密码: ",
+ "authLoginBtn": "登录",
+ "authRegisterBtn": "注册",
+ "authLoginTitle": "登录",
+ "authRegisterTitle": "注册",
+ "launchWithoutAuth": "在未认证的情况下启动",
+
+ "alertInvalid": "无效的用户名或密码",
+ "alertNoPass": "未设置密码,请先更改密码",
+ "alertUnknown": "未知错误,请联系服务器管理员",
+ "alertAuthNoLogin": "未启用认证,无需登录!",
+ "alertLoginSuccess": "登录成功!Token 已复制至剪切板。将 Token 粘贴至用户名处即可登录。",
+
+ "alertUserTaken": "用户名已被占用",
+ "alertPassMismatch": "密码与确认密码不匹配",
+ "alertAuthNoRegister": "未启用认证,无需注册!",
+ "alertRegisterSuccess": "注册成功!",
+
+ "downloadTitle": "下载",
+ "grassclipperTitle": "GrassClipper",
+ "grasscutterTitle": "Grasscutter",
+ "installerTitle": "安装",
+ "installerSubtitle": "安装Grasscutter所需的代理和其他工具。",
+ "downloadStable": "下载Grasscutter稳定版",
+ "stableSubtitle": "安装稳定版的Grasscutter。此版本更加稳定,但是功能相对较少。",
+ "downloadDev": "下载Grasscutter开发版",
+ "devSubtitle": "安装开发版的Grasscutter. 这个版本更新更加频繁,但是有时会有些bug。使用风险自负.",
+ "downloadResources": "下载Grasscutter资源",
+ "resourceSubtitle": "将所需资源下载至当前设置的Grasscutter文件夹。 除非你自己放置了资源,否则你应该执行此操作.",
+
+ "gcScriptRunning": "运行中...",
+ "stableInstall": "下载",
+ "devInstall": "下载",
+
+ "updateNotifText": "有新版本可用!当前最新的版本是: ",
+
+ "chineseCharacterAlert": "当前文件路径含有中文字符,可能会导致一些问题。"
+}
diff --git a/manifest.json b/manifest.json
index 31387d0..830026a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,5 +1,5 @@
{
"applicationId": "js.grassclipper.app",
- "version": "0.6.0",
+ "version": "0.9.4",
"resourcesURL": "https://github.com/Grasscutters/GrassClipper/releases/latest/download/resources.neu"
}
\ No newline at end of file
diff --git a/neutralino.config.json b/neutralino.config.json
index 2d31a3d..015fc6b 100644
--- a/neutralino.config.json
+++ b/neutralino.config.json
@@ -1,6 +1,6 @@
{
"applicationId": "js.grassclipper.app",
- "version": "0.6.0",
+ "version": "0.9.4",
"defaultMode": "window",
"port": 0,
"documentRoot": "/resources/",
@@ -18,6 +18,7 @@
"filesystem.*",
"storage.*",
"window.*",
+ "clipboard.*",
"debug.log"
],
"modes": {
@@ -45,9 +46,21 @@
]
},
"chrome": {
- "width": 1000,
- "height": 800,
- "args": "--user-agent=\"Neutralinojs chrome mode\""
+ "title": "GrassClipper",
+ "width": 1280,
+ "height": 720,
+ "minWidth": 400,
+ "minHeight": 200,
+ "fullScreen": false,
+ "alwaysOnTop": false,
+ "icon": "/resources/icons/appIcon.png",
+ "enableInspector": true,
+ "borderless": true,
+ "maximize": false,
+ "hidden": false,
+ "resizable": false,
+ "exitProcessOnClose": true,
+ "args": "--user-agent=\"Neutralinojs chrome mode\" --disable-web-security"
}
},
"cli": {
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..0b2dccb
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1585 @@
+{
+ "name": "grassclipper",
+ "version": "0.9.1",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "grassclipper",
+ "version": "0.9.1",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "eslint": "^8.14.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz",
+ "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.3.1",
+ "globals": "^13.9.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
+ "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.7.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
+ "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "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"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz",
+ "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/eslintrc": "^1.2.2",
+ "@humanwhocodes/config-array": "^0.9.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.6.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "regexpp": "^3.2.0",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+ "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
+ "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.7.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+ "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
+ "dev": true
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz",
+ "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "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.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/v8-compile-cache": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ }
+ },
+ "dependencies": {
+ "@eslint/eslintrc": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz",
+ "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.3.1",
+ "globals": "^13.9.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ }
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
+ "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "acorn": {
+ "version": "8.7.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
+ "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "requires": {}
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "eslint": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz",
+ "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==",
+ "dev": true,
+ "requires": {
+ "@eslint/eslintrc": "^1.2.2",
+ "@humanwhocodes/config-array": "^0.9.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.6.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "regexpp": "^3.2.0",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ }
+ },
+ "eslint-scope": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+ "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true
+ },
+ "espree": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
+ "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.7.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^3.3.0"
+ }
+ },
+ "esquery": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+ "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.2.0"
+ }
+ },
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.3"
+ }
+ },
+ "globals": {
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz",
+ "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "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.3"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "v8-compile-cache": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
index a92cf81..0d92cb6 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,16 @@
{
- "name": "GrassClipper",
- "version": "0.6.0",
+ "name": "grassclipper",
+ "version": "0.9.4",
"repository": "https://github.com/Grasscutters/GrassClipper.git",
"author": "SpikeHD ",
"license": "Apache-2.0",
"scripts": {
"dev": "neu run",
- "build": "npm run clean_dist && neu build && npm run move_files && npm run move_bgs && npm run rename_exe",
- "move_files": "cp -r ./languages ./dist/GrassClipper && cp -r ./proxy ./dist/GrassClipper && cp -r ./scripts ./dist/GrassClipper",
- "move_bgs": "mkdir dist\\GrassClipper\\resources\\bg\\private && cp -r ./resources/bg/private ./dist/GrassClipper/resources/bg && cp -r ./resources/bg/server ./dist/GrassClipper/resources/bg",
- "rename_exe": "mv ./dist/GrassClipper/GrassClipper-win_x64.exe ./dist/GrassClipper/GrassClipper.exe",
- "clean_dist": "rm -rf ./dist"
+ "build-win": ".\\build_win.cmd",
+ "build-linux": "./build.sh",
+ "build": "echo !! Run build-win or build-linux to build for your platform !!\n"
+ },
+ "devDependencies": {
+ "eslint": "^8.14.0"
}
}
diff --git a/proxy/proxy.py b/proxy/proxy.py
index 841e8fc..50da319 100644
--- a/proxy/proxy.py
+++ b/proxy/proxy.py
@@ -6,7 +6,7 @@
##
#
-# Genshin Impact script for mitmproxy
+# Anime game script for mitmproxy
#
# https://github.com/MlgmXyysd/
#
@@ -24,19 +24,34 @@ import string
from mitmproxy import ctx
from mitmproxy import http
-class MlgmXyysd_Genshin_Impact_Proxy:
+class MlgmXyysd_Anime_Game_Proxy:
def load(self, loader):
loader.add_option(
name = "ip",
typespec = str,
- default = "localhost",
+ default = "127.0.0.1",
help = "IP address to replace",
)
+ loader.add_option(
+ name = "port",
+ typespec = int,
+ default = 443,
+ help = "Port to replace",
+ )
+
+ loader.add_option(
+ name = "use_https",
+ typespec = bool,
+ default = True,
+ help = "Use HTTPS",
+ )
+
def request(self, flow: http.HTTPFlow) -> None:
# This can also be replaced with another IP address.
REMOTE_HOST = ctx.options.ip
+ REMOTE_PORT = ctx.options.port
LIST_DOMAINS = [
"api-os-takumi.mihoyo.com",
@@ -63,12 +78,24 @@ class MlgmXyysd_Genshin_Impact_Proxy:
"sdk-os-static.hoyoverse.com",
"api-account-os.hoyoverse.com",
"hk4e-sdk-os.hoyoverse.com",
- "overseauspider.yuanshen.com"
+ "overseauspider.yuanshen.com",
+ "gameapi-account.mihoyo.com",
+ "minor-api.mihoyo.com",
+ "public-data-api.mihoyo.com",
+ "uspider.yuanshen.com",
+ "sdk-static.mihoyo.com",
+ "abtest-api-data-sg.hoyoverse.com",
+ "log-upload-os.hoyoverse.com"
]
if flow.request.host in LIST_DOMAINS:
+ if ctx.options.use_https:
+ flow.request.scheme = "https"
+ else:
+ flow.request.scheme = "http"
flow.request.host = REMOTE_HOST
+ flow.request.port = REMOTE_PORT
addons = [
- MlgmXyysd_Genshin_Impact_Proxy()
+ MlgmXyysd_Anime_Game_Proxy()
]
\ No newline at end of file
diff --git a/resources/bg/private/1.png b/resources/bg/private/1.png
index 6140560..295812c 100644
Binary files a/resources/bg/private/1.png and b/resources/bg/private/1.png differ
diff --git a/resources/icons/alert.svg b/resources/icons/alert.svg
new file mode 100644
index 0000000..babed01
--- /dev/null
+++ b/resources/icons/alert.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/resources/icons/download.svg b/resources/icons/download.svg
new file mode 100644
index 0000000..8baf9ed
--- /dev/null
+++ b/resources/icons/download.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/resources/icons/folder.svg b/resources/icons/folder.svg
new file mode 100644
index 0000000..077b707
--- /dev/null
+++ b/resources/icons/folder.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/resources/icons/refresh.svg b/resources/icons/refresh.svg
new file mode 100644
index 0000000..cc46601
--- /dev/null
+++ b/resources/icons/refresh.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/resources/index.html b/resources/index.html
index 9766e00..913ec51 100644
--- a/resources/index.html
+++ b/resources/index.html
@@ -2,13 +2,37 @@
+
+
+
+
+
+
+
+
+
+
+ This is a test alert
+
+
+
+
+ Dialog!
+ This is dialog content!
+
+
+
+
+
+
+
@@ -17,6 +41,150 @@
+
+
+
+
+
+ Login
+ Register
+
+
+
+
+
+
+
+
+
+
+ Logging in to
+
+
+
+
+
+ Username:
+
+
+
+
+
+ Password:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Registering for
+
+
+
+
+
+ Username:
+
+
+
+
+
+ Password:
+
+
+
+
+
+ Confirm Password:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Downloads
+
+
+
+
+
+
+ GrassClipper
+
+
+
+ Installer
+
+
+
+ Installs proxy and other tools. Required for Grasscutter servers.
+
+
+
+ Grasscutter
+
+
+
+ Download Grasscutter Stable Build
+
+
+
+ Install Grasscutter stable branch. This build usually has less bugs, but also less features.
+
+
+
+
+ Download Grasscutter Development Build
+
+
+
+ Install Grasscutter development branch. This build sometimes has bugs, and is frequently updated. Use at your own risk.
+
+
+
+
+ Download Grasscutter Resources
+
+
+
+ Downloads Grasscutter resources into the currently set Grasscutter folder. This should be done unless you plan on getting resources externally.
+
+
+
+
+
+
Settings
@@ -74,13 +242,31 @@
Select your language!
+
+
+ Use HTTPS
+
+
+
+
+ Choose between using HTTPS or HTTP.
+
+
+
+
GrassClipper
- 0.4.5
+ 0.0.0
+
+
+
+
+
+
@@ -91,37 +277,59 @@
+
+
+
+
+
+
-
+
+
+
-
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+ A new update is available! Newest version:
+
+
diff --git a/resources/js/alerts.js b/resources/js/alerts.js
new file mode 100644
index 0000000..546ba57
--- /dev/null
+++ b/resources/js/alerts.js
@@ -0,0 +1,19 @@
+function displayAlert(message, clear = 4000) {
+ const alert = document.getElementById('alert')
+ const alertText = document.getElementById('alertText')
+
+ alert.classList.add('show')
+
+ alertText.innerText = message
+
+ setTimeout(() => {
+ hideAlert()
+ }, clear)
+}
+
+function hideAlert() {
+ const alert = document.getElementById('alert')
+ const alertText = document.getElementById('alertText')
+
+ alert.classList.remove('show')
+}
diff --git a/resources/js/authAlert.js b/resources/js/authAlert.js
new file mode 100644
index 0000000..6f2dbf3
--- /dev/null
+++ b/resources/js/authAlert.js
@@ -0,0 +1,45 @@
+let alertTimeout, alertCooldown = 3000
+
+async function displayLoginAlert(message, type, cooldown = null) {
+ displayAuthAlert(message, type, cooldown, 'login')
+}
+
+async function displayRegisterAlert(message, type, cooldown = null) {
+ displayAuthAlert(message, type, cooldown, 'register')
+}
+
+function displayAuthAlert(message, type, cooldown, name) {
+ const elm = document.getElementById(`${name}Alert`)
+ const text = document.getElementById(`${name}AlertText`)
+
+ elm.style.removeProperty('display')
+
+ // Remove classification classes
+ elm.classList.remove('error')
+ elm.classList.remove('success')
+ elm.classList.remove('warn')
+
+ switch(type) {
+ case 'error':
+ elm.classList.add('error')
+ break
+
+ case 'success':
+ elm.classList.add('success')
+ break
+
+ case 'warn':
+ default:
+ elm.classList.add('warn')
+ break
+ }
+
+ text.innerText = message
+
+ clearTimeout(alertTimeout)
+
+ // Disappear after cooldown
+ alertTimeout = setTimeout(() => {
+ elm.style.display = 'none'
+ }, cooldown || alertCooldown)
+}
\ No newline at end of file
diff --git a/resources/js/gcdownloader.js b/resources/js/gcdownloader.js
new file mode 100644
index 0000000..8244c71
--- /dev/null
+++ b/resources/js/gcdownloader.js
@@ -0,0 +1,105 @@
+async function clearGCInstallation() {
+ Neutralino.os.execCommand('del /s /q "./gc"')
+}
+
+async function setDownloadButtonsToLoading() {
+ const stableBtn = document.querySelector('#stableInstall')
+ const devBtn = document.querySelector('#devInstall')
+
+ stableBtn.innerText = localeObj.gcScriptRunning || 'Running...'
+
+ devBtn.innerText = localeObj.gcScriptRunning || 'Running...'
+
+ // Set btns to disabled
+ stableBtn.disabled = true
+ stableBtn.classList.add('disabled')
+
+ devBtn.disabled = true
+ devBtn.classList.add('disabled')
+}
+
+async function resetDownloadButtons() {
+ const stableBtn = document.querySelector('#stableInstall')
+ const devBtn = document.querySelector('#devInstall')
+
+ stableBtn.innerText = localeObj.stableInstall || 'Download'
+ devBtn.innerText = localeObj.devInstall || 'Download'
+
+ // Set btns to enabled
+ stableBtn.disabled = false
+ stableBtn.classList.remove('disabled')
+
+ devBtn.disabled = false
+ devBtn.classList.remove('disabled')
+}
+
+async function downloadGC(branch) {
+ const config = await getCfg()
+
+ // If we are pulling from a new branch, delete the old installation
+ if (config.grasscutterBranch !== branch) await clearGCInstallation()
+
+ // Set current installation in config
+ config.grasscutterBranch = branch
+
+ // Set gc path for people with launcher enabled
+ config.serverFolder = `${NL_CWD}\\gc-${branch}\\grasscutter.jar`
+
+ // Enable server launcher
+ config.serverLaunchPanel = true
+
+ Neutralino.storage.setData('config', JSON.stringify(config))
+
+ setDownloadButtonsToLoading()
+
+ // Keystore for branch (since they can differ)
+ const keystoreUrl = `https://github.com/Grasscutters/Grasscutter/raw/${branch}/keystore.p12`
+
+ // External service that allows un-authed artifact downloading
+ const artiUrl = `https://nightly.link/Grasscutters/Grasscutter/workflows/build/${branch}/Grasscutter.zip`
+
+ // For data files
+ const dataFiles = await axios.get(`https://api.github.com/repos/Grasscutters/Grasscutter/contents/data?ref=${branch}`)
+ const dataList = dataFiles.data
+ .map(file => ({ path: file.path, filename: file.name }))
+ .map(o => ({ url: `https://raw.githubusercontent.com/Grasscutters/Grasscutter/${branch}/${o.path}`, filename: o.filename }))
+
+ // For key files
+ const keyFiles = await axios.get(`https://api.github.com/repos/Grasscutters/Grasscutter/contents/keys?ref=${branch}`)
+ const keyList = keyFiles.data
+ .map(file => ({ path: file.path, filename: file.name }))
+ .map(o => ({ url: `https://raw.githubusercontent.com/Grasscutters/Grasscutter/${branch}/${o.path}`, filename: o.filename }))
+
+ const serverFolderFixed = config.serverFolder.match(/.*\\|.*\//g, '')[0].replace(/\//g, '\\')
+
+ // Ensure data and key folders exist
+
+ console.log(config.serverFolder)
+ console.log(serverFolderFixed)
+
+ await Neutralino.os.execCommand(`mkdir ${serverFolderFixed}\\data`)
+ await Neutralino.os.execCommand(`mkdir ${serverFolderFixed}\\keys`)
+
+ // Download data files
+ for (const o of dataList) {
+ const folder = 'data'
+ await Neutralino.os.execCommand(`powershell Invoke-WebRequest -Uri ${o.url} -OutFile "${serverFolderFixed}\\${folder}\\${o.filename}"`)
+ }
+
+ // Download key files
+ for (const o of keyList) {
+ const folder = 'keys'
+ await Neutralino.os.execCommand(`powershell Invoke-WebRequest -Uri ${o.url} -OutFile "${serverFolderFixed}\\${folder}\\${o.filename}"`)
+ }
+
+ // Run installer
+ createCmdWindow(`.\\scripts\\gc_download.cmd ${artiUrl} ${keystoreUrl} ${branch}`)
+
+ // Fix buttons
+ resetDownloadButtons()
+
+ // Display folder after saving config
+ displayServerFolder()
+ enableServerButton()
+ displayServerLaunchSection()
+}
\ No newline at end of file
diff --git a/resources/js/helpers.js b/resources/js/helpers.js
new file mode 100644
index 0000000..558d91a
--- /dev/null
+++ b/resources/js/helpers.js
@@ -0,0 +1,169 @@
+/**
+ * Get configuration
+ *
+ * @returns {Promise}
+ */
+async function getCfg() {
+ const defaultConf = {
+ gameexe: '',
+ serverFolder: '',
+ lastConnect: '',
+ enableKillswitch: false,
+ serverLaunchPanel: false,
+ language: 'en',
+ useHttps: true,
+ grasscutterBranch: '',
+ }
+ const cfgStr = await Neutralino.storage.getData('config').catch(e => {
+ // The data isn't set, so this is our first time opening
+ Neutralino.storage.setData('config', JSON.stringify(defaultConf))
+
+ // Show the first time notice if there is no config
+ document.querySelector('#firstTimeNotice').style.display = 'block'
+ })
+
+ const config = cfgStr ? JSON.parse(cfgStr) : defaultConf
+
+ return config
+}
+
+/**
+ * Get the list of favorite IPs
+ *
+ * @returns {Promise}
+ */
+async function getFavIps() {
+ const ipStr = await Neutralino.storage.getData('favorites').catch(e => {
+ // The data isn't set, so this is our first time opening
+ Neutralino.storage.setData('favorites', JSON.stringify([]))
+ })
+
+ const ipArr = ipStr ? JSON.parse(ipStr) : []
+
+ return ipArr
+}
+
+async function proxyIsInstalled() {
+ // Check if the proxy server is installed
+ const curDirList = await filesystem.readDirectory(NL_CWD)
+
+ if (curDirList.find(f => f.entry === 'ext')) {
+ const extFiles = await filesystem.readDirectory(NL_CWD + '/ext')
+
+ if (extFiles.find(f => f.entry === 'mitmdump.exe')) {
+ return true
+ }
+ }
+
+ return false
+}
+
+async function checkForUpdates() {
+ const url = 'https://api.github.com/repos/Grasscutters/GrassClipper/releases/latest'
+
+ const { data } = await axios.get(url)
+ const latest = data.tag_name
+
+ return latest
+}
+
+async function displayUpdate() {
+ const latest = await checkForUpdates()
+ const versionDisplay = document.querySelector('#newestVersion')
+ const notif = document.querySelector('#downloadNotif')
+
+ if (latest === `v${NL_APPVERSION}`) return
+
+ versionDisplay.innerText = latest
+
+ notif.classList.add('displayed')
+
+ setTimeout(() => {
+ notif.classList.remove('displayed')
+ }, 5000)
+}
+
+async function openLatestDownload() {
+ const downloadLink = 'https://github.com/Grasscutters/GrassClipper/releases/latest/'
+
+ Neutralino.os.open(downloadLink)
+}
+
+async function openGameFolder() {
+ const config = await getCfg()
+ const folder = config.gameexe.match(/.*\\|.*\//g, '')
+
+ if (folder.length > 0) openInExplorer(folder[0].replace(/\//g, '\\'))
+}
+
+async function openGrasscutterFolder() {
+ const config = await getCfg()
+ const folder = config.serverFolder.match(/.*\\|.*\//g, '')
+
+ if (folder.length > 0) openInExplorer(folder[0].replace(/\//g, '\\'))
+}
+
+// https://www.jimzhao.us/2015/09/javascript-detect-chinese-character.html
+function hasChineseChars(str) {
+ let re1 = new RegExp(/[\u4E00-\uFA29]/) //Chinese character range
+ let re2 = new RegExp(/[\uE7C7-\uE7F3]/) //non Chinese character range
+ str = str.replace(/\s/g, '')
+
+ if (!re1.test(str) || re2.test(str)) {
+ return false
+ }
+
+ return true
+}
+
+function openDialog(title, message, negBtn = false, affirmBtn = closeDialog) {
+ const dialog = document.getElementById('miscDialog')
+ const titleElm = document.getElementById('dialogTitle')
+ const contents = document.getElementById('dialogContent')
+ const noBtn = document.getElementById('dialogButtonNeg')
+ const yesBtn = document.getElementById('dialogButtonAffirm')
+
+ if (!negBtn) {
+ noBtn.style.display = 'none'
+ } else {
+ noBtn.style.removeProperty('display')
+ noBtn.onclick = () => closeDialog()
+ }
+
+ yesBtn.innerText = localeObj.dialogYes || 'OK'
+ noBtn.innerText = localeObj.dialogNo || 'NO'
+
+ yesBtn.onclick = () => {
+ affirmBtn()
+ closeDialog()
+ }
+
+ // Set title and message
+ titleElm.innerText = title
+ contents.innerText = message
+
+ // Show the dialog
+ dialog.style.display = 'block'
+}
+
+function closeDialog() {
+ const dialog = document.getElementById('miscDialog')
+
+ dialog.style.display = 'none'
+}
+
+/**
+ * Minimize the window
+ */
+function minimizeWin() {
+ Neutralino.window.minimize()
+}
+
+/**
+ * Close the window
+ */
+function closeWin() {
+ Neutralino.app.exit()
+
+ window.close()
+}
diff --git a/resources/js/index.js b/resources/js/index.js
index 290a957..4c77c6c 100644
--- a/resources/js/index.js
+++ b/resources/js/index.js
@@ -1,127 +1,15 @@
-Neutralino.init();
+Neutralino.init()
-let localeObj;
+NL_CWD = NL_CWD.replace(/\//g, '\\')
+
+let localeObj
const filesystem = Neutralino.filesystem
-
-/**
- * Every autofill, such as backgrounds and the game folder,
- * should be done here to ensure DOM contents are loaded.
- */
-document.addEventListener('DOMContentLoaded', async () => {
- setBackgroundImage();
- displayGenshinFolder();
- displayServerFolder();
-
- // Set title version
- document.querySelector('#version').innerHTML = NL_APPVERSION
-
- const config = await getCfg()
- const ipArr = await getFavIps()
-
- if (config.serverLaunchPanel) {
- displayServerLaunchSection()
- }
-
- // Set last connect
- document.querySelector('#ip').value = config.lastConnect
-
- if (ipArr.includes(config.lastConnect)) {
- document.querySelector('#star').src = 'icons/star_filled.svg'
- }
-
- // Disable private game launch if proxy IP or proxy server is not found
- const playPriv = document.querySelector('#playPrivate')
-
- if (!(await proxyIsInstalled())) {
- playPriv.classList.add('disabled')
- playPriv.disabled = true
- }
-
- // Exit favorites list and settings panel when clicking outside of it
- window.addEventListener("click", function(e) {
- const favList = document.querySelector('#ipList')
- const settingsPanel = document.querySelector('#settingsPanel')
-
- // This will close the favorites list no matter what is clicked
- if (favList.style.display !== 'none') {
- favList.style.display = 'none'
- favList.style.transform = ''
- }
-
- // This will close the settings panel no matter what is clicked
- let settingCheckElm = e.target
-
- while(settingCheckElm.tagName !== 'BODY') {
- if (settingCheckElm.id === 'settingsPanel'
- || settingCheckElm.id === 'settingsBtn') {
- return
- }
-
- settingCheckElm = settingCheckElm.parentElement
- }
-
- // We travelled through the parents, so if we are at the body, we clicked outside of the settings panel
- if (settingCheckElm.tagName === 'BODY') {
- // This will close the settings panel only when something outside of it is clicked
- if (settingsPanel.style.display !== 'none') {
- settingsPanel.style.display = 'none'
- }
- }
- });
-
- // Ensure we do the translation at the very end, after everything else has loaded
- await doTranslation()
-
- if (!config.genshinImpactFolder) {
- handleGenshinFolderNotSet()
- }
-
- if (!config.serverFolder) {
- handleServerNotSet()
- }
-})
-
-/**
- * Get the list of favorite IPs
- *
- * @returns {Promise}
- */
-async function getFavIps() {
- const ipStr = await Neutralino.storage.getData('favorites').catch(e => {
- // The data isn't set, so this is our first time opening
- Neutralino.storage.setData('favorites', JSON.stringify([]))
- })
-
- const ipArr = ipStr ? JSON.parse(ipStr) : []
-
- return ipArr
+const createCmdWindow = async (command) => {
+ Neutralino.os.execCommand(`cmd.exe /c start "" ${command}`, { background: true })
}
-/**
- * Get configuration
- *
- * @returns {Promise}
- */
-async function getCfg() {
- const defaultConf = {
- genshinImpactFolder: '',
- serverFolder: '',
- lastConnect: '',
- enableKillswitch: false,
- serverLaunchPanel: false,
- language: 'en'
- }
- const cfgStr = await Neutralino.storage.getData('config').catch(e => {
- // The data isn't set, so this is our first time opening
- Neutralino.storage.setData('config', JSON.stringify(defaultConf))
-
- // Show the first time notice if there is no config
- document.querySelector('#firstTimeNotice').style.display = 'block'
- })
-
- const config = cfgStr ? JSON.parse(cfgStr) : defaultConf
-
- return config
+const openInExplorer = async (path) => {
+ createCmdWindow(`explorer.exe "${path}"`)
}
/**
@@ -144,7 +32,7 @@ async function enableButtons() {
/**
* Enable server launch button
*/
- async function enableServerButton() {
+async function enableServerButton() {
const serverBtn = document.querySelector('#serverLaunch')
serverBtn.classList.remove('disabled')
@@ -154,12 +42,12 @@ async function enableButtons() {
/**
* Disable buttons when the game folder is not set
*/
-async function handleGenshinFolderNotSet() {
+async function handleGameNotSet() {
// Set buttons to greyed out and disable
- document.querySelector('#genshinPath').innerHTML = localeObj.folderNotSet
+ document.querySelector('#gamePath').innerHTML = localeObj.folderNotSet
// Set official server background to default
- document.querySelector('#firstPanel').style.backgroundImage = `url("../bg/private/default.png")`
+ document.querySelector('#firstPanel').style.backgroundImage = 'url("../bg/private/default.png")'
const offBtn = document.querySelector('#playOfficial')
const privBtn = document.querySelector('#playPrivate')
@@ -186,35 +74,20 @@ async function handleServerNotSet() {
privBtn.disabled = true
}
-async function proxyIsInstalled() {
- // Check if the proxy server is installed
- const curDirList = await filesystem.readDirectory(NL_CWD)
-
- if (curDirList.find(f => f.entry === 'ext')) {
- const extFiles = await filesystem.readDirectory(NL_CWD + '/ext')
-
- if (extFiles.find(f => f.entry === 'mitmdump.exe')) {
- return true
- }
- }
-
- return false
-}
-
/**
* Show the game folder under the select button
*/
-async function displayGenshinFolder() {
- const elm = document.querySelector('#genshinPath')
+async function displayGameFolder() {
+ const elm = document.querySelector('#gamePath')
const config = await getCfg()
- elm.innerHTML = config.genshinImpactFolder
+ elm.innerHTML = config.gameexe
}
/**
* Show the server folder under the select button
*/
- async function displayServerFolder() {
+async function displayServerFolder() {
const elm = document.querySelector('#serverPath')
const config = await getCfg()
@@ -234,7 +107,7 @@ async function setBackgroundImage() {
const servImage = servImages[Math.floor(Math.random() * servImages.length)].entry
// Set default image, it will change if the bg folder exists
- document.querySelector('#firstPanel').style.backgroundImage = `url("https://webstatic.hoyoverse.com/upload/event/2020/11/04/7fd661b5184e1734f91f628b6f89a31f_7367318474207189623.png")`
+ document.querySelector('#firstPanel').style.backgroundImage = 'url("https://webstatic.hoyoverse.com/upload/event/2020/11/04/7fd661b5184e1734f91f628b6f89a31f_7367318474207189623.png")'
// Set the private background image
document.querySelector('#secondPanel').style.backgroundImage = `url("../bg/private/${privImage}")`
@@ -262,17 +135,17 @@ async function setBackgroundImage() {
await filesystem.createDirectory(NL_CWD + '/resources/bg/official')
}
- if (config.genshinImpactFolder) {
+ if (config.gameexe) {
// See if bg folder exists in parent dir
- const parentDir = await filesystem.readDirectory(config.genshinImpactFolder + '/..')
+ const parentDir = await filesystem.readDirectory(config.gameexe + '/..')
if (parentDir.find(dir => dir.entry === 'bg')) {
- const officialImages = (await filesystem.readDirectory(config.genshinImpactFolder + '/../bg')).filter(file => file.type === 'FILE')
+ const officialImages = (await filesystem.readDirectory(config.gameexe + '/../bg')).filter(file => file.type === 'FILE')
if (officialImages.length > 0) {
for (const bg of officialImages) {
- const path = config.genshinImpactFolder.replace('\\', '/') + '/../bg/' + bg.entry
+ const path = config.gameexe.replace('\\', '/') + '/../bg/' + bg.entry
// See if the file exists already
const currentBgs = (await filesystem.readDirectory(NL_CWD + '/resources/bg/official/')).filter(file => file.type === 'FILE')
@@ -300,9 +173,12 @@ async function setBackgroundImage() {
*/
async function handleFavoriteInput() {
const ip = document.querySelector('#ip').value
+ const port = document.querySelector('#port').value || '443'
const ipArr = await getFavIps()
- if (!ip || !ipArr.includes(ip)) {
+ const addr = `${ip}:${port}`
+
+ if (!ip || !ipArr.includes(addr)) {
document.querySelector('#star').src = 'icons/star_empty.svg'
} else {
document.querySelector('#star').src = 'icons/star_filled.svg'
@@ -316,13 +192,18 @@ async function handleFavoriteInput() {
*/
async function setIp(ip) {
const ipInput = document.querySelector('#ip')
+ const portInput = document.querySelector('#port')
+
+ const parseIp = ip.split(':')[0]
+ const parsePort = ip.split(':')[1]
// Set star
if (ip) {
document.querySelector('#star').src = 'icons/star_filled.svg'
}
- ipInput.value = ip
+ ipInput.value = parseIp
+ portInput.value = parsePort
}
/**
@@ -358,41 +239,36 @@ async function handleFavoriteList() {
const transform = window.getComputedStyle(document.querySelector('#ipList')).transform
const xy = [ transform.split(',')[4], transform.split(',')[5] ]
- let newY = parseInt(xy[1].replace(')', '')) - (27 * ipArr.length)
+ let newY = (27 * ipArr.length) * window.devicePixelRatio
- if (ipArr.length === 0) newY -= 27
+ if (ipArr.length === 0 || ipArr.length === 1) newY = 0
- ipList.style.transform = `translate(${xy[0]}px, ${newY}px)`
+ ipList.style.transform = `translate(${xy[0]}px, calc(56vh - ${newY}px)`
}
}
-/**
- * Add the current value of the IP input to the favorites list
- * OR
- * Remove the current value of the IP input from the favorites list
- */
-async function setFavorite() {
- const ip = document.querySelector('#ip').value
- const ipArr = await getFavIps()
+async function openDownloads() {
+ const downloads = document.querySelector('#downloadPanel')
+ const config = await getCfg()
- // Set star icon
- const star = document.querySelector('#star')
-
- if (star.src.includes('filled') && ip) {
- star.src = 'icons/star_empty.svg'
-
- // remove from list
- ipArr.splice(ipArr.indexOf(ip), 1)
- } else {
- star.src = 'icons/star_filled.svg'
-
- // add to list
- if (ip && !ipArr.includes(ip)) {
- ipArr.push(ip)
- }
+ if (downloads.style.display === 'none') {
+ downloads.style.removeProperty('display')
}
- Neutralino.storage.setData('favorites', JSON.stringify(ipArr))
+ // Disable the resource download button if a serverFolder path is not set
+ if (!config.serverFolder) {
+ document.querySelector('#resourceInstall').disabled = true
+ document.querySelector('#resourceInstall').classList.add('disabled')
+ } else {
+ document.querySelector('#resourceInstall').disabled = false
+ document.querySelector('#resourceInstall').classList.remove('disabled')
+ }
+}
+
+async function closeDownloads() {
+ const downloads = document.querySelector('#downloadPanel')
+
+ downloads.style.display = 'none'
}
async function openSettings() {
@@ -406,9 +282,11 @@ async function openSettings() {
// Fill setting options with what is currently set in config
const killSwitch = document.querySelector('#killswitchOption')
const serverLaunch = document.querySelector('#serverLaunchOption')
+ const httpsCheckbox = document.querySelector('#httpsOption')
killSwitch.checked = config.enableKillswitch
serverLaunch.checked = config.serverLaunchPanel
+ httpsCheckbox.checked = config.useHttps
// Load languages
getLanguages()
@@ -424,7 +302,7 @@ async function closeSettings() {
settings.style.display = 'none'
// In case we installed the proxy server
- if (await proxyIsInstalled() && config.genshinImpactFolder) {
+ if (await proxyIsInstalled() && config.gameexe) {
const playPriv = document.querySelector('#playPrivate')
playPriv.classList.remove('disabled')
@@ -432,13 +310,44 @@ async function closeSettings() {
}
}
-async function toggleKillSwitch() {
- const killSwitch = document.querySelector('#killswitchOption')
+async function openLogin() {
+ const login = document.querySelector('#loginPanel')
+ const ip = document.querySelector('#ip').value
+ const port = document.querySelector('#port').value
+ const loginIpDisplay = document.querySelector('#loginPopupServer')
+ const registerIpDisplay = document.querySelector('#registerPopupServer')
+
const config = await getCfg()
+ const useHttps = config.useHttps
+ const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}`
- config.enableKillswitch = killSwitch.checked
+ // Check if we even need to authenticate
+ try {
+ const { data } = await axios.get(url + '/authentication/type')
- Neutralino.storage.setData('config', JSON.stringify(config))
+ if (!data.includes('GCAuthAuthenticationHandler')) {
+ launchPrivate()
+ return
+ }
+ } catch(e) {
+ launchPrivate()
+ return
+ }
+
+ loginIpDisplay.innerText = ip
+ registerIpDisplay.innerText = ip
+
+ if (login.style.display === 'none') {
+ login.style.removeProperty('display')
+ }
+}
+
+async function closeLogin() {
+ const login = document.querySelector('#loginPanel')
+
+ login.style.display = 'none'
+
+ setLoginSection()
}
async function closeFirstTimePopup() {
@@ -447,7 +356,15 @@ async function closeFirstTimePopup() {
}
async function runInstallScript() {
- Neutralino.os.execCommand(`${NL_CWD}/scripts/install.cmd "${NL_CWD}"`)
+ createCmdWindow(`.\\scripts\\install.cmd "${NL_CWD}" true`)
+
+ // Create an interval that will check for the proxy server installation finish
+ const interval = setInterval(async () => {
+ if (await proxyIsInstalled()) {
+ clearInterval(interval)
+ enableButtons()
+ }
+ }, 1000)
closeFirstTimePopup()
}
@@ -464,10 +381,10 @@ async function checkForUpdatesAndShow() {
// Version mismatch? Update!
if (manifest?.version !== NL_APPVERSION) {
- subtitle.innerHTML = "New update available!"
+ subtitle.innerHTML = 'New update available!'
updateBtn.classList.remove('disabled')
} else {
- subtitle.innerHTML = "You are on the latest version! :)"
+ subtitle.innerHTML = 'You are on the latest version! :)'
updateBtn.classList.add('disabled')
}
}
@@ -485,157 +402,87 @@ async function displayServerLaunchSection() {
}
}
-async function toggleServerLaunchSection() {
- const config = await getCfg()
-
- displayServerLaunchSection()
-
- // Save setting
- config.serverLaunchPanel = !config.serverLaunchPanel
- Neutralino.storage.setData('config', JSON.stringify(config))
-}
-
-async function getLanguages() {
- const languageFiles = (await filesystem.readDirectory(`${NL_CWD}/languages`)).filter(file => file.entry.endsWith('.json'))
- const config = await getCfg()
-
- // Load all languages as options
- for (const file of languageFiles) {
- const fullLanguageName = JSON.parse(await filesystem.readFile(`${NL_CWD}/languages/${file.entry}`)).fullLangName
- const lang = file.entry.split('.json')[0]
-
- const option = document.createElement('option')
- option.value = lang
- option.innerHTML = fullLanguageName
-
- // Set language selected to config language
- if (lang === config.language) {
- option.selected = true
- }
-
- document.querySelector('#languageSelect').appendChild(option)
- }
-
-}
-
-async function handleLanguageChange(elm) {
- const list = elm
- const config = await getCfg()
-
- // Set language in config
- config.language = list.value
- Neutralino.storage.setData('config', JSON.stringify(config))
-
- // Force refresh of application, no need for restart!
- window.location.reload()
-}
-
/**
* Set the game folder by opening a folder picker
*/
-async function setGenshinImpactFolder() {
- const folder = await Neutralino.os.showFolderDialog(localeObj.genshinFolderDialog)
+async function setGameExe() {
+ const gameExe = await Neutralino.os.showOpenDialog(localeObj.gameFolderDialog, {
+ filters: [
+ { name: 'Executable files', extensions: ['exe'] }
+ ]
+ })
+
+ if (!gameExe[0]) return
+ if (hasChineseChars(gameExe[0])) displayAlert(localeObj.chineseCharacterAlert)
// Set the folder in our configuration
const config = await getCfg()
- // See if the actual game folder is inside this one
- const folderList = await filesystem.readDirectory(folder)
- const gameFolder = folderList.filter(file => file.entry.includes('Genshin Impact Game'))
-
- if (gameFolder.length > 0) {
- config.genshinImpactFolder = folder + '\\Genshin Impact Game'
- Neutralino.storage.setData('config', JSON.stringify(config))
- } else {
- config.genshinImpactFolder = folder
- }
+ // It's an array of selections, so only get the first one
+ config.gameexe = gameExe[0].replace(/\//g, '\\')
Neutralino.storage.setData('config', JSON.stringify(config))
// Refresh background and path
setBackgroundImage()
- displayGenshinFolder()
+ displayGameFolder()
enableButtons()
}
-async function setGrassCutterFolder() {
+async function setGrasscutterFolder() {
const folder = await Neutralino.os.showOpenDialog(localeObj.grasscutterFileDialog, {
filters: [
{ name: 'Jar files', extensions: ['jar'] }
]
})
+ if (!folder[0]) return
+ if (hasChineseChars(folder[0])) displayAlert(localeObj.chineseCharacterAlert)
+
// Set the folder in our configuration
const config = await getCfg()
- config.serverFolder = folder
+ config.serverFolder = folder[0]
Neutralino.storage.setData('config', JSON.stringify(config))
displayServerFolder()
enableServerButton()
}
-/**
- * Get the name of the game executable
- *
- * @returns {Promise}
- */
-async function getGenshinExecName() {
- // Scan genshin dir
- const config = await getCfg()
- const genshinDir = await filesystem.readDirectory(config.genshinImpactFolder)
-
- // Find the executable
- const genshinExec = genshinDir.find(file => file.entry.endsWith('.exe'))
-
- return genshinExec.entry
-}
-
/**
* Launch the game with no modifications nor proxy
*/
async function launchOfficial() {
const config = await getCfg()
- Neutralino.os.execCommand(config.genshinImpactFolder + '/' + await getGenshinExecName())
+ Neutralino.os.execCommand(`"${config.gameexe}"`)
}
/**
* Launch the game with a proxy
*/
async function launchPrivate() {
- const ip = document.getElementById('ip').value || 'localhost'
+ const ip = document.getElementById('ip').value || '127.0.0.1'
+ const port = document.getElementById('port').value || '443'
const config = await getCfg()
- console.log('connecting to ' + ip)
+ console.log('connecting to ' + ip + ':' + port)
// Set the last connect
config.lastConnect = ip
Neutralino.storage.setData('config', JSON.stringify(config))
// Pass IP and game folder to the private server launcher
- Neutralino.os.execCommand(`${NL_CWD}/scripts/private_server_launch.cmd ${ip} "${config.genshinImpactFolder}/${await getGenshinExecName()}" "${NL_CWD}" ${config.enableKillswitch}`).catch(e => console.log(e))
+ Neutralino.os.execCommand(
+ `.\\scripts\\private_server_launch.cmd ${ip} ${port} ${config.useHttps} "${config.gameexe}" "${NL_CWD}" ${config.enableKillswitch} true`, {
+ background: true
+ }
+ ).catch(e => console.log(e))
}
async function launchLocalServer() {
const config = await getCfg()
- Neutralino.os.execCommand(`${NL_CWD}/scripts/local_server_launch.cmd "${config.serverFolder}"`).catch(e => console.log(e))
+ createCmdWindow(`.\\scripts\\local_server_launch.cmd "${config.serverFolder}"`).catch(e => console.log(e))
}
-
-/**
- * Minimize the window
- */
-function minimizeWin() {
- console.log('min')
- Neutralino.window.minimize()
-}
-
-/**
- * Close the window
- */
-function closeWin() {
- console.log('close')
- Neutralino.app.exit()
-}
\ No newline at end of file
diff --git a/resources/js/login.js b/resources/js/login.js
new file mode 100644
index 0000000..0982456
--- /dev/null
+++ b/resources/js/login.js
@@ -0,0 +1,154 @@
+/**
+ * Toggle the login section
+ */
+async function setLoginSection() {
+ const title = document.getElementById('loginSectionTitle')
+ const altTitle = document.getElementById('registerSectionTitle')
+ const loginSection = document.getElementById('loginPopupContentBody')
+ const registerSection = document.getElementById('registerPopupContentBody')
+
+ title.classList.add('selectedTitle')
+ altTitle.classList.remove('selectedTitle')
+
+ loginSection.style.removeProperty('display')
+ registerSection.style.display = 'none'
+}
+
+/**
+ * Toggle the register section
+ */
+async function setRegisterSection(fromLogin = false) {
+ const title = document.getElementById('registerSectionTitle')
+ const altTitle = document.getElementById('loginSectionTitle')
+ const loginSection = document.getElementById('loginPopupContentBody')
+ const registerSection = document.getElementById('registerPopupContentBody')
+
+ title.classList.add('selectedTitle')
+ altTitle.classList.remove('selectedTitle')
+
+ loginSection.style.display = 'none'
+ registerSection.style.removeProperty('display')
+
+ if (fromLogin) {
+ // Take the values from the login section and put them in the register section
+ const loginUsername = document.getElementById('loginUsername').value
+ const loginPassword = document.getElementById('loginPassword').value
+
+ document.getElementById('registerUsername').value = loginUsername
+ document.getElementById('registerPassword').value = loginPassword
+ }
+}
+
+function parseJwt(token) {
+ const base64Url = token.split('.')[1]
+ const base64 = base64Url.replace('-', '+').replace('_', '/')
+ return JSON.parse(window.atob(base64))
+}
+
+/**
+ * Attempt login and launch game
+ */
+async function login() {
+ const username = document.getElementById('loginUsername').value
+ const password = document.getElementById('loginPassword').value
+ const ip = document.getElementById('ip').value
+ const port = document.getElementById('port').value || '443'
+ const config = await getCfg()
+ const useHttps = config.useHttps
+ const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}`
+
+ const reqBody = {
+ username,
+ password,
+ }
+
+ const { data } = await axios.post(url + '/authentication/login', reqBody)
+
+ switch(data.message) {
+ case 'INVALID_ACCOUNT':
+ displayLoginAlert(localeObj.alertInvalid || 'Invalid username or password', 'error')
+ break
+
+ case 'NO_PASSWORD':
+ // No account password, create one with change password
+ displayLoginAlert(localeObj.alertNoPass || 'No password set, please change password', 'warn')
+ break
+
+ case 'UNKNOWN':
+ // Unknown error, contact server owner
+ displayLoginAlert(localeObj.alertUnknown || 'Unknown error, contact server owner', 'error')
+ break
+
+ case undefined:
+ case null:
+ case 'AUTH_DISABLED':
+ // Authentication is disabled, we can just connect the user
+ displayLoginAlert(localeObj.alertAuthNoLogin || 'Authentication is disabled, no need to log in!', 'warn')
+ launchPrivate()
+ break
+
+ default:
+ // Success! Copy the JWT token to their clipboard
+ const tkData = parseJwt(data.jwt)
+ await Neutralino.clipboard.writeText(tkData.token)
+
+ displayLoginAlert(localeObj.alertLoginSuccess || 'Login successful! Token copied to clipboard. Paste this token into the username field of the game to log in.', 'success', 8000)
+ launchPrivate()
+ break
+ }
+}
+
+/**
+ * Attempt registration, do not launch game
+ */
+async function register() {
+ const username = document.getElementById('registerUsername').value
+ const password = document.getElementById('registerPassword').value
+ const password_confirmation = document.getElementById('registerPasswordConfirm').value
+ const ip = document.getElementById('ip').value
+ const port = document.getElementById('port').value || '443'
+ const config = await getCfg()
+ const useHttps = config.useHttps
+ const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}`
+
+ const reqBody = {
+ username,
+ password,
+ password_confirmation
+ }
+
+ const { data } = await axios.post(url + '/authentication/register', reqBody)
+
+ switch(data.message) {
+ case 'USERNAME_TAKEN':
+ // Username is taken
+ displayRegisterAlert(localeObj.alertUserTaken || 'Username is taken', 'error')
+ break
+
+ case 'PASSWORD_MISMATCH':
+ // The password and password confirmation do not match
+ displayRegisterAlert(localStorage.alertPassMismatch || 'Password and password confirmation do not match', 'error')
+ break
+
+ case 'UNKNOWN':
+ // Unknown error, contact server owner
+ displayRegisterAlert(localeObj.alertUnknown || 'Unknown error, contact server owner', 'error')
+ break
+
+ case undefined:
+ case null:
+ case 'AUTH_DISABLED':
+ // Authentication is disabled, we can just connect the user
+ displayRegisterAlert(localeObj.alertAuthNoRegister || 'Authentication is disabled, no need to register!', 'warn')
+ break
+
+ default:
+ // Success!! Bring them to the login screen and auto-input their username
+ const loginUsername = document.getElementById('loginUsername')
+ loginUsername.value = username
+
+ setLoginSection()
+ displayLoginAlert(localeObj.alertRegisterSuccess || 'Registration successful!', 'success', 5000)
+ break
+ }
+}
diff --git a/resources/js/onLoad.js b/resources/js/onLoad.js
new file mode 100644
index 0000000..2065d5e
--- /dev/null
+++ b/resources/js/onLoad.js
@@ -0,0 +1,89 @@
+
+/**
+ * Every autofill, such as backgrounds and the game folder,
+ * should be done here to ensure DOM contents are loaded.
+ */
+document.addEventListener('DOMContentLoaded', async () => {
+ displayUpdate()
+ setBackgroundImage()
+ displayGameFolder()
+ displayServerFolder()
+
+ // Set title version
+ document.querySelector('#version').innerHTML = NL_APPVERSION
+
+ const config = await getCfg()
+ const ipArr = await getFavIps()
+
+ if (config.serverLaunchPanel) {
+ displayServerLaunchSection()
+ }
+
+ // Set last connect
+ document.querySelector('#ip').value = config.lastConnect
+
+ if (ipArr.includes(config.lastConnect)) {
+ document.querySelector('#star').src = 'icons/star_filled.svg'
+ }
+
+ // Disable private game launch if proxy IP or proxy server is not found
+ const playPriv = document.querySelector('#playPrivate')
+
+ if (!(await proxyIsInstalled())) {
+ playPriv.classList.add('disabled')
+ playPriv.disabled = true
+ }
+
+ // Exit favorites list and settings panel when clicking outside of it
+ window.addEventListener('click', function(e) {
+ const favList = document.querySelector('#ipList')
+ const settingsPanel = document.querySelector('#settingsPanel')
+ const downloadPanel = document.querySelector('#downloadPanel')
+
+ // This will close the favorites list no matter what is clicked
+ if (favList.style.display !== 'none') {
+ favList.style.display = 'none'
+ favList.style.transform = ''
+ }
+
+ // This will close the settings panel no matter what is clicked
+ let checkElm = e.target
+
+ while(checkElm.tagName !== 'BODY') {
+ if (checkElm.id === 'settingsPanel'
+ || checkElm.id === 'settingsBtn') {
+ return
+ }
+
+ if (checkElm.id === 'downloadPanel' ||
+ checkElm.id === 'downloadBtn') {
+ return
+ }
+
+ checkElm = checkElm.parentElement
+ }
+
+ // We travelled through the parents, so if we are at the body, we clicked outside of the settings panel
+ if (checkElm.tagName === 'BODY') {
+ // This will close the settings panel only when something outside of it is clicked
+ if (settingsPanel.style.display !== 'none') {
+ settingsPanel.style.display = 'none'
+ }
+
+ if (downloadPanel.style.display !== 'none') {
+ downloadPanel.style.display = 'none'
+ }
+ }
+ })
+
+ // Ensure we do the translation at the very end, after everything else has loaded
+ await doTranslation()
+
+ if (!config.gameexe) {
+ handleGameNotSet()
+ }
+
+ if (!config.serverFolder) {
+ handleServerNotSet()
+ }
+})
diff --git a/resources/js/options.js b/resources/js/options.js
new file mode 100644
index 0000000..f18d2cf
--- /dev/null
+++ b/resources/js/options.js
@@ -0,0 +1,126 @@
+/**
+ * Toggle the killswitch script
+ */
+async function toggleKillSwitch() {
+ const killSwitch = document.querySelector('#killswitchOption')
+ const config = await getCfg()
+
+ config.enableKillswitch = killSwitch.checked
+
+ Neutralino.storage.setData('config', JSON.stringify(config))
+}
+
+/**
+ * Toggles the server launching panel
+ */
+async function toggleServerLaunchSection() {
+ const config = await getCfg()
+
+ displayServerLaunchSection()
+
+ // Save setting
+ config.serverLaunchPanel = !config.serverLaunchPanel
+ Neutralino.storage.setData('config', JSON.stringify(config))
+
+ // Show a dialog for those who may want to open the downloads section
+ if (config.serverLaunchPanel && !config.serverFolder) {
+ closeSettings()
+
+ openDialog(
+ localeObj.serverEnableDialogTitle || 'You found the Grasscutter server launcher!',
+ localeObj.serverEnableDialogText || 'If you do not have an existing Grasscutter installation to set, would you like to download a build?',
+ true,
+ openDownloads
+ )
+ }
+}
+
+/**
+ * Get all languages for the language selector
+ */
+async function getLanguages() {
+ const languageFiles = (await filesystem.readDirectory(`${NL_CWD}/languages`)).filter(file => file.entry.endsWith('.json'))
+ const config = await getCfg()
+
+ // Clear language options
+ const languageSelect = document.querySelector('#languageSelect')
+ languageSelect.innerHTML = ''
+
+ // Load all languages as options
+ for (const file of languageFiles) {
+ const fullLanguageName = JSON.parse(await filesystem.readFile(`${NL_CWD}/languages/${file.entry}`)).fullLangName
+ const lang = file.entry.split('.json')[0]
+
+ const option = document.createElement('option')
+ option.value = lang
+ option.innerHTML = fullLanguageName
+
+ // Set language selected to config language
+ if (lang === config.language) {
+ option.selected = true
+ }
+
+ document.querySelector('#languageSelect').appendChild(option)
+ }
+}
+
+/**
+ * Save lang, refresh to apply
+ *
+ * @param {DOMElement} elm
+ */
+async function handleLanguageChange(elm) {
+ const list = elm
+ const config = await getCfg()
+
+ // Set language in config
+ config.language = list.value
+ Neutralino.storage.setData('config', JSON.stringify(config))
+
+ // Force refresh of application, no need for restart!
+ window.location.reload()
+}
+
+/**
+ * Toggle the use of HTTPS
+ */
+async function toggleHttps() {
+ const httpsCheckbox = document.querySelector('#httpsOption')
+ const config = await getCfg()
+
+ config.useHttps = httpsCheckbox.checked
+
+ Neutralino.storage.setData('config', JSON.stringify(config))
+}
+
+/**
+ * Add the current value of the IP input to the favorites list
+ * OR
+ * Remove the current value of the IP input from the favorites list
+ */
+async function setFavorite() {
+ const ip = document.querySelector('#ip').value
+ const port = document.querySelector('#port').value || '443'
+ const ipArr = await getFavIps()
+
+ const addr = `${ip}:${port}`
+
+ // Set star icon
+ const star = document.querySelector('#star')
+
+ if (star.src.includes('filled') && ip) {
+ star.src = 'icons/star_empty.svg'
+
+ // remove from list
+ ipArr.splice(ipArr.indexOf(addr), 1)
+ } else {
+ star.src = 'icons/star_filled.svg'
+
+ // add to list
+ if (ip && !ipArr.includes(addr)) {
+ ipArr.push(addr)
+ }
+ }
+
+ Neutralino.storage.setData('favorites', JSON.stringify(ipArr))
+}
diff --git a/resources/js/translation.js b/resources/js/translation.js
index 1d24191..a579a5f 100644
--- a/resources/js/translation.js
+++ b/resources/js/translation.js
@@ -13,40 +13,51 @@ async function doTranslation() {
}
const localization = await filesystem.readFile(`${NL_CWD}/languages/${config.language}.json`)
+ const engLocale = await filesystem.readFile(`${NL_CWD}/languages/en.json`)
+ engLocaleObj = JSON.parse(engLocale)
localeObj = JSON.parse(localization)
- const set = (id, localeString) => document.getElementById(id).innerHTML = localeString || 'UNKNOWN'
+ const set = (id, localeString) => document.getElementById(id).innerText = localeObj[localeString] || engLocaleObj[localeString]
// Begin filling in values
- set('titleSection', localeObj.appName)
+ set('titleSection', 'appName')
+
+ const verSpan = document.createElement('span')
+ verSpan.id = 'version'
+ verSpan.innerHTML = ` v${NL_APPVERSION}`
+
+ document.querySelector('#titleSection').appendChild(verSpan)
// Play buttons
- set('playOfficial', localeObj.playOfficial)
- set('playPrivate', localeObj.playPrivate)
- set('serverLaunch', localeObj.launchLocalServer)
+ set('playOfficial', 'playOfficial')
+ set('playPrivate', 'playPrivate')
+ set('serverLaunch', 'launchLocalServer')
// File select buttons
- set('genshinFolderSet', localeObj.genshinFolderSet)
- set('grasscutterFileSet', localeObj.grasscutterFileSet)
+ set('gameExeSet', 'gameExeSet')
+ set('grasscutterFileSet', 'grasscutterFileSet')
// Private options
- set('ip', localeObj.ipPlaceholder)
+ document.querySelector('#ip').placeholder = localeObj.ipPlaceholder
+ document.querySelector('#port').placeholder = localeObj.portPlaceholder
// Settings
- set('fullSettingsTitle', localeObj.settingsTitle)
- set('scriptsTitle', localeObj.scriptsSectionTitle)
- set('killswitchTitle', localeObj.killswitchOption)
- set('killswitchSubtitle', localeObj.killswitchSubtitle)
- set('proxyTitle', localeObj.proxyOption)
- set('proxyInstall', localeObj.proxyInstallBtn)
- set('proxySubtitle', localeObj.proxySubtitle)
- set('updateBtn', localeObj.updateOption)
- set('updateTitle', localeObj.updateOption)
- set('updateSubtitle', localeObj.updateSubtitle)
- set('languageTitle', localeObj.languageOption)
- set('languageSubtitle', localeObj.languageSubtitle)
- set('serverLaunchTitle', localeObj.enableServerLauncherOption)
- set('serverSubtitle', localeObj.enableServerLauncherSubtitle)
+ set('fullSettingsTitle', 'settingsTitle')
+ set('scriptsTitle', 'scriptsSectionTitle')
+ set('killswitchTitle', 'killswitchOption')
+ set('killswitchSubtitle', 'killswitchSubtitle')
+ set('proxyTitle', 'proxyOption')
+ set('proxyInstall', 'proxyInstallBtn')
+ set('proxySubtitle', 'proxySubtitle')
+ set('updateBtn', 'updateOption')
+ set('updateTitle', 'updateOption')
+ set('updateSubtitle', 'updateSubtitle')
+ set('languageTitle', 'languageOption')
+ set('languageSubtitle', 'languageSubtitle')
+ set('serverLaunchTitle', 'enableServerLauncherOption')
+ set('serverSubtitle', 'enableServerLauncherSubtitle')
+ set('httpsTitle', 'httpsOption')
+ set('httpsSubtitle', 'httpsSubtitle')
// Intro popup
const popup = document.getElementById('firstTimeNotice')
@@ -62,6 +73,38 @@ async function doTranslation() {
introSpan.innerHTML += localeObj.introSen3 + ' '
introSpan.innerHTML += localeObj.introSen4 + ' '
- set('firstTimeInstallBtn', localeObj.proxyInstallBtn)
- set('firstTimeDenyBtn', localeObj.proxyInstallDeny)
+ set('firstTimeInstallBtn', 'proxyInstallBtn')
+ set('firstTimeDenyBtn', 'proxyInstallDeny')
+
+ // Login section
+ set('loginSectionTitle', 'authLoginTitle')
+ set('registerSectionTitle', 'authRegisterTitle')
+ set('loggingInToIndicator', 'loggingInTo')
+ set('registeringToIndicator', 'registeringFor')
+ set('loginUsernameIndicator', 'authUsername')
+ set('loginPasswordIndicator', 'authPassword')
+ set('registerUsernameIndicator', 'authUsername')
+ set('registerPasswordIndicator', 'authPassword')
+ set('registerConfirmIndicator', 'authConfirmPassword')
+ set('loginPopupContentBodyBtnLogin', 'authLoginBtn')
+ set('loginPopupContentBodyBtnRegister', 'authRegisterBtn')
+ set('noLoginBtn', 'launchWithoutAuth')
+
+ // Downloads section
+ set('downloadTitle', 'downloadTitle')
+ set('grassclipperTitle', 'grassclipperTitle')
+ set('grasscutterTitle', 'grasscutterTitle')
+ set('installerTitle', 'installerTitle')
+ set('installerSubtitle', 'installerSubtitle')
+ set('downloadStable', 'downloadStable')
+ set('stableSubtitle', 'stableSubtitle')
+ set('downloadDev', 'downloadDev')
+ set('devSubtitle', 'devSubtitle')
+ set('downloadResources', 'downloadResources')
+ set('resourceSubtitle', 'resourceSubtitle')
+ set('stableInstall', 'stableInstall')
+ set('devInstall', 'devInstall')
+
+ // update notification
+ set('updateNotifText', 'updateNotifText')
}
\ No newline at end of file
diff --git a/resources/js/windowDrag.js b/resources/js/windowDrag.js
index 811ca68..642b0f3 100644
--- a/resources/js/windowDrag.js
+++ b/resources/js/windowDrag.js
@@ -1,22 +1,25 @@
// https://stackoverflow.com/questions/67971689/positioning-the-borderless-window-in-neutralino-js
// had to use this since the in-built function breaks the close and minimize buttons
-let dragging = false, posX, posY;
-let draggable;
+let dragging = false, ratio = 1, posX, posY
+let draggable
-document.addEventListener('DOMContentLoaded', () => {
- draggable = document.getElementById('controlBar');
+document.addEventListener('DOMContentLoaded', async () => {
+ draggable = document.getElementById('controlBar')
// Listen to hovers
draggable.onmousedown = function (e) {
- posX = e.pageX, posY = e.pageY;
- dragging = true;
+ ratio = window.devicePixelRatio
+
+ posX = e.pageX * ratio, posY = e.pageY * ratio
+ dragging = true
}
- draggable.onmouseup = function (e) {
- dragging = false;
+ // Patch for monitors with scaling enabled, allows them to detach from the titlebar anywhere
+ window.onmouseup = function (e) {
+ dragging = false
}
document.onmousemove = function (e) {
- if (dragging) Neutralino.window.move(e.screenX - posX, e.screenY - posY);
+ if (dragging) Neutralino.window.move(e.screenX * ratio - posX, e.screenY * ratio - posY)
}
})
\ No newline at end of file
diff --git a/resources/style/index.css b/resources/style/index.css
index e7d2b6c..893576f 100644
--- a/resources/style/index.css
+++ b/resources/style/index.css
@@ -7,6 +7,14 @@ body {
font-family: Arial, Helvetica, sans-serif;
}
+a {
+ color: #fff;
+}
+
+img {
+ height: 20px;
+}
+
.darken {
filter: brightness(0.6);
}
@@ -27,13 +35,135 @@ body {
margin: 10px;
}
+#miscDialog span,
#firstTimeNotice span {
display: block;
text-align: center;
margin-bottom: 6px;
}
+#loginPanel {
+ height: 50%;
+ width: 32%;
+}
+
+#loginPanel img {
+ height: 20px;
+}
+
+#loginPopupTitle {
+ display: flex;
+ justify-content: space-between;
+ font-size: 1.2em;
+ font-weight: bold;
+}
+
+.authBody {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+}
+
+.authBody div {
+ margin: 6px;
+}
+
+.authBody input {
+ height: 20px;
+ background: white;
+ border: none;
+ border-bottom: 2px solid #4d4d4d;
+
+ /* border bottom anim */
+ transition: border-bottom 0.1s ease-in-out;
+}
+
+.authBody input:focus {
+ outline: none;
+ border-bottom: 2px solid #ffc61e;
+}
+
+#loginPopupTitle img {
+ height: 20px;
+ margin-bottom: 8px;
+}
+
+#loginPopupTitle img:hover {
+ filter: invert(85%) sepia(31%) saturate(560%) hue-rotate(329deg) brightness(100%) contrast(92%);
+ cursor: pointer;
+}
+
+#registerPopupServer,
+#loginPopupServer {
+ font-weight: bold;
+}
+
+#loginSectionTitle,
+#registerSectionTitle {
+ font-weight: normal;
+ font-size: 1em;
+}
+
+#loginSectionTitle:hover,
+#registerSectionTitle:hover {
+ cursor: pointer;
+}
+
+.selectedTitle {
+ font-weight: bold !important;
+ border-bottom: 2px solid #ffc61e;
+}
+
+.authInputs div {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.authInputs div span {
+ margin-right: 6px;
+}
+
+.error {
+ background: #e90000;
+}
+
+.warn {
+ background: #ffc61e;
+}
+
+.success {
+ background: #00c200;
+}
+
+#registerAlert,
+#loginAlert {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 5px;
+ color: #fff;
+ text-align: center;
+}
+
+#registerAlert img,
+#loginAlert img {
+ height: 20px;
+ margin: 10px;
+ filter: invert(100%) sepia(0%) saturate(179%) hue-rotate(253deg) brightness(105%) contrast(101%);
+}
+
+#registerAlert span,
+#loginAlert span {
+ margin: 10px;
+}
+
+#miscDialog,
#firstTimeNotice,
+#loginPanel,
+#downloadPanel,
#settingsPanel {
display: block;
position: absolute;
@@ -49,17 +179,21 @@ body {
font-family: system-ui;
}
+#downloadPanel,
#settingsPanel {
width: 35%;
- height: 70%;
+ height: 80%;
+ overflow: auto;
}
+#downloadTitle,
#fullSettingsTitle {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 10px;
}
+#downloadPanelInner,
#settingsPanelInner {
display: flex;
flex-direction: column;
@@ -68,16 +202,23 @@ body {
padding: 10px 10%;
}
+.downloadRow,
.settingsRow {
width: 100%;
}
+.downloadTitle,
.settingTitle {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 10px;
}
+.downloadTitle {
+ margin: 6px;
+}
+
+.downloadLabel,
.settingLabel {
display:inline-block;
font-size: 1em;
@@ -85,12 +226,14 @@ body {
margin: 10px 0px;
}
+.downloadSubtitle,
.settingSubtitle {
color: rgb(165, 165, 165);
font-size: 0.8em;
font-weight: normal;
}
+.downloadSection,
.settingSection {
display: flex;
flex-direction: row;
@@ -98,10 +241,12 @@ body {
justify-content: space-between;
}
+.downloadSection .smolBtn,
.settingSection .smolBtn {
height: 30px;
}
+#downloadTitleBar,
#settingsTitleBar {
display: flex;
flex-direction: row;
@@ -109,15 +254,18 @@ body {
justify-content: space-between;
}
+#downloadClose,
#settingsClose {
display: inline-block;
transition: filter 0.1s ease-in-out;
}
+#downloadClose img,
#settingsClose img {
height: 20px;
}
+#downloadClose:hover,
#settingsClose:hover {
filter: invert(85%) sepia(31%) saturate(560%) hue-rotate(329deg) brightness(100%) contrast(92%);
cursor: pointer;
@@ -127,7 +275,7 @@ body {
position: absolute;
z-index: 99;
padding: 10px;
- transform: translate(150px, 420px);
+ transform: translate(3vw, 420px);
background-color: #fff;
border-radius: 5px;
border: 1px solid #ccc;
@@ -276,6 +424,18 @@ body {
background: linear-gradient(#ffc61e, #ffd326);
}
+.openFolderIcon {
+ display: inline;
+ height: 20px;
+ filter: invert(97%) sepia(85%) saturate(12%) hue-rotate(184deg) brightness(103%) contrast(103%);
+ padding: 10px;
+}
+
+.openFolderIcon:hover {
+ cursor: pointer;
+ filter: invert(99%) sepia(0%) saturate(1092%) hue-rotate(172deg) brightness(80%) contrast(103%);
+}
+
#bottomBar {
display: flex;
justify-content: center;
@@ -304,7 +464,13 @@ body {
height: 10%;
}
-#genshinPath, #serverPath {
+.bottomSection div div {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+}
+
+#gamePath, #serverPath {
color: white;
font-size: 14px;
}
@@ -336,7 +502,7 @@ body {
}
#firstPanel {
- background-position: -340px;
+ background-position: -200px;
}
/* Move the first official button to the position on the png */
@@ -351,6 +517,10 @@ body {
display: block;
}
+#port {
+ width: 12%;
+}
+
#secondPanel input {
margin-bottom: 4px;
height: 20px;
@@ -393,4 +563,82 @@ body {
#serverInput img:hover {
cursor: pointer;
+}
+
+#downloadNotif {
+ z-index: 99;
+ position: absolute;
+
+ color: #fff;
+ background: #141414;
+ border: 1px solid #ccc;
+
+ padding: 1em;
+ top: 80%;
+ left: 120%;
+ width: 20%;
+ border-radius: 5px;
+
+ text-align: center;
+
+ transition: left 0.5s ease-in-out;
+}
+
+#downloadNotif:hover {
+ cursor: pointer;
+ border-color: #fff;
+}
+
+#downloadNotif img {
+ height: 20px;
+ vertical-align: middle;
+ margin-right: 4px;
+ filter: invert(100%) sepia(100%) saturate(0%) hue-rotate(16deg) brightness(103%) contrast(101%);
+}
+
+#downloadNotif.displayed {
+ left: 70%;
+}
+
+#newestVersion {
+ font-weight: bold;
+}
+
+#alert {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ font-weight: bold;
+ color: #704a1d;
+
+ position: absolute;
+ top: -100%;
+ left: 15%;
+ background: linear-gradient(#ffc61e, #ffd326);
+ border: 1px solid #704a1d;
+ border-radius: 6px;
+
+ height: 50px;
+ width: 70%;
+
+ z-index: 999;
+
+ transition: top 0.15s ease-in-out;
+}
+
+#alert.show {
+ top: 6%;
+}
+
+#dialogTitle {
+ font-weight: bold;
+}
+
+#dialogBtns {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ width: 100%;
+ margin: 0.6em;
}
\ No newline at end of file
diff --git a/scripts/gc_download.cmd b/scripts/gc_download.cmd
new file mode 100644
index 0000000..4e8e3f3
--- /dev/null
+++ b/scripts/gc_download.cmd
@@ -0,0 +1,58 @@
+@echo off
+
+set KEYSTORE_URL=%1
+set ARTIFACT_URL=%2
+set BRANCH=%3
+set FOLDER_NAME=".\gc-%BRANCH%"
+set FOLDER_NAME=%FOLDER_NAME:"=%
+
+title GC Download Script
+
+if not exist "%FOLDER_NAME%" mkdir "%FOLDER_NAME%"
+if not exist ".\temp" mkdir ".\temp"
+
+echo Downloading Grasscutter prebuilt jar...
+
+:: Download the jar
+powershell Invoke-WebRequest -Uri %KEYSTORE_URL% -OutFile ".\temp\gcjar.zip"
+
+echo Extracting...
+
+:: Delete old file if there is one there
+if exist "%FOLDER_NAME%\grasscutter.jar" del "%FOLDER_NAME%\grasscutter.jar"
+
+powershell Expand-Archive -Path ".\temp\gcjar.zip" -DestinationPath "%FOLDER_NAME%" -Force
+
+:: Find the jar file name and rename it, just in case
+for %%i in (%FOLDER_NAME%/*) do (
+ :: If the extension is jar, rename the file
+ if %%~xi equ .jar rename "%FOLDER_NAME%\%%i" grasscutter.jar
+)
+
+echo Downloading keystore.p12...
+
+:: Download the keystore.p12 file
+powershell Invoke-WebRequest -Uri %ARTIFACT_URL% -OutFile ".\%FOLDER_NAME%\keystore.p12"
+
+:: Check java version, this will automatically output some tuff
+call .\scripts\javaver.cmd %BRANCH%
+
+:: Allow resource downloading to be optional, since it takes a while
+set REPLY=y
+set /p "REPLY=Download server resources? (This can take a while) [y|n]:"
+if /i not "%reply%" == "y" goto :finish
+
+call .\scripts\resources_download.cmd %FOLDER_NAME%
+
+goto :finish
+
+:finish
+ :: Remove temp stuff
+ del /s /q "./temp"
+
+ echo Done, latest Grasscutter %BRANCH% now downloaded in %FOLDER_NAME%
+
+ pause
+
+ taskkill /f /fi "WINDOWTITLE eq GC Download Script"
+
diff --git a/scripts/install.cmd b/scripts/install.cmd
index 0e29eca..4ccd2f9 100644
--- a/scripts/install.cmd
+++ b/scripts/install.cmd
@@ -3,11 +3,7 @@
set ORIGIN=%1
set ORIGIN=%ORIGIN:"=%
-:: Ensure admin
->nul 2>&1 reg query "HKU\S-1-5-19" || (
- set params = %*:"="""%
- cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 "%1" ", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
-)
+title Grassclipper Installer
echo Downloading proxy server...
@@ -15,22 +11,22 @@ echo Downloading proxy server...
cd "%ORIGIN%"
if not exist "%ORIGIN%/ext" mkdir "%ORIGIN%/ext"
-if not exist "%ORIGIN%/temp" mkdir "%ORIGIN%/temp"
+if not exist "%ORIGIN%/temp" mkdir "%ORIGIN%/temp
-:: Begin by retrieving mitmproxy 8.0.0
-powershell Invoke-WebRequest -Uri https://snapshots.mitmproxy.org/8.0.0/mitmproxy-8.0.0-windows.zip -OutFile "%ORIGIN%/temp/mitmproxy-8.0.0-windows.zip"
+:: Begin by retrieving mitmproxy 7.0.4
+powershell Invoke-WebRequest -Uri https://snapshots.mitmproxy.org/7.0.4/mitmproxy-7.0.4-windows.zip -OutFile "'%ORIGIN%\temp\mitmproxy-7.0.4-windows.zip'"
echo Extracting...
:: Extract from temp/ to ext/ with powershell
-powershell Expand-Archive -Path "%ORIGIN%/temp/mitmproxy-8.0.0-windows.zip" -DestinationPath "%ORIGIN%/ext/" -Force
+powershell Expand-Archive -Path "'%ORIGIN%\temp\mitmproxy-7.0.4-windows.zip'" -DestinationPath "'%ORIGIN%\ext\'" -Force
del /s /q "%ORIGIN%/temp"
echo Running proxy server in order to generate certificates...
:: Start proxy server
-start "Proxy Server" %ORIGIN%/ext/mitmdump.exe --ssl-insecure --set ip=%ip%
+start "Proxy Server" "%ORIGIN%\ext\mitmdump.exe" --ssl-insecure --set ip=%ip%
:: Allow the proxy server to create the certificates
ping 127.0.0.1 -n 6 > nul
@@ -41,12 +37,18 @@ taskkill /f /im mitmdump.exe
echo Adding ceritifcate...
:: Ensure we are elevated for certs
->nul 2>&1 certutil -addstore root %USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer || (
- echo Certificate install failed, ensure the script is running as Administrator and that the path "%USERPROFILE%\.mitmproxy" exists,
+>nul 2>&1 certutil -addstore root "%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer" || (
+ echo ============================================================================================================
+ echo !! Certificate install failed !!
+ echo.
+ echo Please manually run this command as Administrator:
+ echo certutil -addstore root "%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer"
+ echo ============================================================================================================
)
echo Done! You can now open GrassClipper.exe!
pause
-exit /b
\ No newline at end of file
+taskkill /f /fi "WINDOWTITLE eq Grassclipper Installer"
+taskkill /f /fi "WINDOWTITLE eq Administrator: Grassclipper Installer"
\ No newline at end of file
diff --git a/scripts/javaver.cmd b/scripts/javaver.cmd
new file mode 100644
index 0000000..c22093a
--- /dev/null
+++ b/scripts/javaver.cmd
@@ -0,0 +1,70 @@
+@echo off
+
+set BRANCH=%1
+
+echo Checking java version...
+
+where java >nul 2>nul
+if %errorlevel%==1 (
+ echo =======================================================================================
+ echo No version of Java was found!
+
+ if %BRANCH% EQU stable (
+ echo To launch the stable branch server, you must install Java 8
+ )
+
+ if %BRANCH% EQU development (
+ echo To launch the development branch server, you must install Java 17
+ )
+
+ echo =======================================================================================
+
+ exit /b
+)
+
+:: https://stackoverflow.com/questions/5675459/how-to-get-java-version-from-batch-script
+for /f "tokens=3" %%g in ('java -version 2^>^&1 ^| findstr /i "version"') do (
+ @echo Output: %%g
+ set JAVAVER=%%g
+)
+set JAVAVER=%JAVAVER:"=%
+
+for /f "delims=. tokens=1-3" %%v in ("%JAVAVER%") do (
+ set MAJOR=%%v
+ set MINOR=%%w
+ set BUILD=%%x
+)
+
+if %BRANCH% EQU stable (
+ :: Ensure java 8
+ if %MAJOR% EQU 1 (
+ if %MINOR% LSS 8 (
+ echo =======================================================================================
+ echo !! Java version is less than 8 !!
+ echo Please download Java 8 to ensure %BRANCH% branch server launches correctly.
+ echo =======================================================================================
+ exit /b
+ )
+ )
+
+ if %MAJOR% NEQ 1 (
+ echo =======================================================================================
+ echo !! Java version is not 8 !!
+ echo Please download Java 8 to ensure %BRANCH% branch server launches correctly.
+ echo =======================================================================================
+ exit /b
+ )
+)
+
+if %BRANCH% EQU development (
+ :: Ensure java 17
+ if %MAJOR% LSS 17 (
+ echo =======================================================================================
+ echo !! Java version is less than 17 !!
+ echo Please download Java 17 to ensure %BRANCH% branch server launches correctly.
+ echo =======================================================================================
+ exit /b
+ )
+)
+
+echo Java version is compatible
\ No newline at end of file
diff --git a/scripts/killswitch.cmd b/scripts/killswitch.cmd
index 0c8e8ec..8a1cfd0 100644
--- a/scripts/killswitch.cmd
+++ b/scripts/killswitch.cmd
@@ -5,6 +5,9 @@ set GAME_EXE_NAME=%GAME_EXE_NAME:"=%
set PROXY_IP=%2
set PROXY_IP=%PROXY_IP:"=%
+:: For task killing
+title PS Killswitch
+
:: Get current wifi SSID to reconnect
for /f "delims=: tokens=2" %%n in ('netsh wlan show interface name="Wi-Fi" ^| findstr "Profile"') do set "WIFI=%%n"
set WIFI=%WIFI: =%
@@ -19,16 +22,20 @@ if "%PROXY_IP%" EQU "localhost" (
)
:loop
+ :: Wait a couple seconds
+ ping 127.0.0.1 -n 2 > nul
+
:: Check if the game is even running
- @rem QPROCESS "%GAME_EXE_NAME%">NUL
- @rem IF %ERRORLEVEL% NEQ 0 (
- @rem exit /b
- @rem )
+ :: tasklist /fi "ImageName eq %GAME_EXE_NAME%" /fo csv 2>NUL | find /I "%GAME_EXE_NAME%.exe">NUL
+ :: IF %ERRORLEVEL% NEQ 0 (
+ :: exit /b
+ :: )
:: Check if the proxy server process is running
:: https://stackoverflow.com/questions/162291/how-to-check-if-a-process-is-running-via-a-batch-script
tasklist /fi "ImageName eq mitmdump.exe" /fo csv 2>NUL | find /I "mitmdump.exe">NUL
if "%ERRORLEVEL%" NEQ "0" (
+ echo "mitmdump not running"
goto killgame
)
@@ -59,8 +66,6 @@ if "%PROXY_IP%" EQU "localhost" (
goto killgame
)
- timeout /t 2 /NOBREAK >nul
-
goto loop
:killgame
@@ -84,4 +89,6 @@ if "%PROXY_IP%" EQU "localhost" (
:: Reconnect to the WiFi
netsh wlan connect name="%WIFI%"
+ taskkill /f /fi "WINDOWTITLE eq Administrator: PS Killswitch"
+
exit
\ No newline at end of file
diff --git a/scripts/local_server_launch.cmd b/scripts/local_server_launch.cmd
index d411daa..bb98c32 100644
--- a/scripts/local_server_launch.cmd
+++ b/scripts/local_server_launch.cmd
@@ -3,12 +3,27 @@
set GRASSCUTTER_JAR=%1
set GRASSCUTTER_JAR=%GRASSCUTTER_JAR:"=%
-:: Ensure admin
->nul 2>&1 reg query "HKU\S-1-5-19" || (
- set params = %*:"="""%
- cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 "%1"", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
-)
+title Grasscutter
+
+:: Get folder the jar is in
+set "X=%GRASSCUTTER_JAR%"
+:l
+ set IS_SLASH=false
+
+ if "%X:~-1%"=="\" set IS_SLASH=true
+ if "%X:~-1%"=="/" set IS_SLASH=true
+
+ if %IS_SLASH% equ true goto al
+
+ set "X=%X:~0,-1%"
+ goto l
+:al
+ set "X=%X:~0,-1%"
+ set "GRASSCUTTER_ROOT=%X%"
echo Starting local Grasscutter server...
-start /b java -jar %GRASSCUTTER_JAR%
\ No newline at end of file
+:: Change dir to server directory
+cd /d "%GRASSCUTTER_ROOT%"
+
+call java -jar "%GRASSCUTTER_JAR%"
\ No newline at end of file
diff --git a/scripts/private_server_launch.cmd b/scripts/private_server_launch.cmd
index a9ea591..5f78002 100644
--- a/scripts/private_server_launch.cmd
+++ b/scripts/private_server_launch.cmd
@@ -1,19 +1,38 @@
@echo off
:: Ensure admin
->nul 2>&1 reg query "HKU\S-1-5-19" || (
- set params = %*:"="""%
- cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 %1 "%2" ""%cd%"" %4", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
-)
+@REM >nul 2>&1 reg query "HKU\S-1-5-19" || (
+@REM set params = %*:"="""%
+@REM cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 %1 %2 %3 "%4" ""%cd%"" %6", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
+@REM )
+
+:: Use to force task kill
+title PS Launcher Script
+
+:: Use a character encoding that allows for use of Chinese characters. This should work but doesn't, but I'm keeping it in here just in case
+chcp 65001
echo Starting Proxy Server
set IP=%1
-set GAME_PATH=%2
+set PORT=%2
+set USE_HTTPS=%3
+set GAME_PATH=%4
set GAME_PATH=%GAME_PATH:"=%
-set ORIGIN=%3
+set ORIGIN=%5
set ORIGIN=%ORIGIN:"=%
-set ENABLE_KILLSWITCH=%4
+set ENABLE_KILLSWITCH=%6
+
+:: For registry
+set GAME_REG="HKEY_CURRENT_USER\Software\miHoYo\Genshin Impact"
+
+if "%ENABLE_KILLSWITCH%" EQU "true" (
+ :: Restart in elevated if need be
+ >nul 2>&1 reg query "HKU\S-1-5-19" || (
+ set params = %*:"="""%
+ cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 %1 %2 %3 "%4" ""%cd%"" %6", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && taskkill /f /fi "WINDOWTITLE eq PS Launcher Script" && exit /b )
+ )
+)
set PROXY=true
@rem Store original proxy settings
@@ -25,11 +44,11 @@ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v Pr
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "127.0.0.1:8080" /f >nul 2>nul
:: Start proxy server
-start "Proxy Server" %ORIGIN%/ext/mitmdump.exe -s "%ORIGIN%/proxy/proxy.py" --ssl-insecure --set ip=%IP%
+start "Proxy Server" "%ORIGIN%\ext\mitmdump.exe" -s "%ORIGIN%/proxy/proxy.py" -k --allow-hosts ".*\.yuanshen\.com|.*\.mihoyo\.com|.*\.hoyoverse\.com" --ssl-insecure --set ip=%IP% --set port=%PORT% --set use_https=%USE_HTTPS%
echo Opening %GAME_PATH%
-:: Allow the proxy server to create the certificates
+:: Allow the proxy server to open fully
ping 127.0.0.1 -n 5 > nul
for %%A in ("%GAME_PATH%") do (
@@ -41,7 +60,7 @@ echo Killswitch: %ENABLE_KILLSWITCH%
if "%ENABLE_KILLSWITCH%" EQU "true" (
echo Killswitch is enabled!
:: Start killswitch
- start /b %ORIGIN%\scripts\killswitch.cmd "%GAME_EXE%" %IP%"
+ start /b %ORIGIN%\scripts\killswitch.cmd "%GAME_EXE%" 127.0.0.1
)
:: Launch game
@@ -49,16 +68,29 @@ if "%ENABLE_KILLSWITCH%" EQU "true" (
:: On exit clean proxy stuff
:EXIT
-if "%PROXY%" == "" (
+echo Exiting...
+
+if "%PROXY%" EQU "" (
echo Proxy not started, no need to clean up.
-) else (
- :: Clean proxy settings
- echo Cleaning up proxy settings
- reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d "%ORIG_PROXY_ENABLE%" /f >nul 2>nul
- reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "%ORIG_PROXY_SERVER%" /f >nul 2>nul
- :: Kill proxy server
- taskkill /f /im mitmdump.exe
+ exit /b
+)
- exit /b
-)
\ No newline at end of file
+:: Clean proxy settings
+echo Cleaning up proxy settings...
+reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d "%ORIG_PROXY_ENABLE%" /f >nul 2>nul
+reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "%ORIG_PROXY_SERVER%" /f >nul 2>nul
+
+:: Kill proxy server
+taskkill /f /im mitmdump.exe
+
+echo Done, see you next time
+
+:: Just in case the user has corutils installed, use this hacky timeout instead of the timeout command
+ping 127.0.0.1 -n 2 > nul
+
+:: Attempt to kill either
+taskkill /f /fi "WINDOWTITLE eq Administrator: PS Launcher Script"
+taskkill /f /fi "WINDOWTITLE eq PS Launcher Script"
+
+exit /b
\ No newline at end of file
diff --git a/scripts/resources_download.cmd b/scripts/resources_download.cmd
new file mode 100644
index 0000000..d5e14e6
--- /dev/null
+++ b/scripts/resources_download.cmd
@@ -0,0 +1,29 @@
+@echo off
+
+set FOLDER_NAME=%1
+set FOLDER_NAME=%FOLDER_NAME:"=%
+
+if not exist ".\temp" mkdir ".\temp"
+if not exist ".\resources" mkdir ".\resources"
+
+echo Downloading resources, this can take a while...
+
+:: Grab the giant ass resource zip
+powershell Invoke-WebRequest -Uri https://github.com/Koko-boya/Grasscutter_Resources/archive/refs/heads/main.zip -OutFile ".\temp\resources.zip"
+
+echo Extracting...
+
+:: Extract resources to the folder
+powershell Expand-Archive -Path ".\temp\resources.zip" -DestinationPath "%FOLDER_NAME%" -Force
+
+:: Delete old resources folder if there is one there
+del /s /q "%FOLDER_NAME%\resources">nul
+
+echo Moving resources to folder...
+
+robocopy "%FOLDER_NAME%\Grasscutter_Resources-main\Resources" "%FOLDER_NAME%\resources" /E /MOVE>nul
+
+:: Delete straggling files
+del /s /q "%FOLDER_NAME%\Grasscutter_Resources-main"
+
+echo Done, resources should be properly extracted
\ No newline at end of file
diff --git a/setup_win.cmd b/setup_win.cmd
new file mode 100644
index 0000000..6f110b4
--- /dev/null
+++ b/setup_win.cmd
@@ -0,0 +1,8 @@
+@echo off
+
+set BINARY_URL="https://github.com/SpikeHD/neutralinojs/releases/download/v1337.0.0/neutralino-win_x64.exe"
+
+call npm install
+
+:: Use powershell to download the binary
+powershell Invoke-WebRequest -Uri %BINARY_URL% -OutFile "./bin/neutralino-win_x64.exe"
\ No newline at end of file
diff --git a/tools/mtools.exe b/tools/mtools.exe
new file mode 100644
index 0000000..87141d4
Binary files /dev/null and b/tools/mtools.exe differ