mirror of
https://github.com/Grasscutters/Cultivation.git
synced 2025-12-14 16:14:48 +01:00
Compare commits
101 Commits
v1.0.3-alp
...
v1.0.6-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01fce477ef | ||
|
|
2f610ce0af | ||
|
|
d9772d9ffb | ||
|
|
93097db5cc | ||
|
|
c06aa9f885 | ||
|
|
f7bcad0a90 | ||
|
|
7120f846f7 | ||
|
|
f6ed37d2e4 | ||
|
|
99b210b3be | ||
|
|
7d52766c07 | ||
|
|
212d974ac6 | ||
|
|
a6e06e3005 | ||
|
|
56c1f2dcc2 | ||
|
|
a843888cb8 | ||
|
|
281bfb5cea | ||
|
|
79891238b6 | ||
|
|
0971f5b826 | ||
|
|
7e5f3be4fa | ||
|
|
e29e269c4c | ||
|
|
8f8e37aff3 | ||
|
|
6257a2e68c | ||
|
|
203bd40e8f | ||
|
|
c64cdababa | ||
|
|
1ecd38ee9f | ||
|
|
26292984a2 | ||
|
|
cffbcdae96 | ||
|
|
2026e2f896 | ||
|
|
9566beaf29 | ||
|
|
c99080168c | ||
|
|
9fa3351747 | ||
|
|
75f1eef587 | ||
|
|
43a6348b7a | ||
|
|
cc74107dfe | ||
|
|
afa40f437f | ||
|
|
95282a3f36 | ||
|
|
0331bb5faf | ||
|
|
0551f3e6a0 | ||
|
|
412acdd317 | ||
|
|
c42c708db5 | ||
|
|
552d612e7c | ||
|
|
5077c19fdc | ||
|
|
36c2302f1b | ||
|
|
a06a8af7df | ||
|
|
975b04fd0e | ||
|
|
811f437238 | ||
|
|
a25645ef77 | ||
|
|
ac8fd3af45 | ||
|
|
4f3952aeb1 | ||
|
|
c0740417e3 | ||
|
|
8700a77ba0 | ||
|
|
95267720a4 | ||
|
|
d97e5c192f | ||
|
|
a9d9d361e1 | ||
|
|
b78d9c28c9 | ||
|
|
f946cedb4d | ||
|
|
c659979851 | ||
|
|
940943b106 | ||
|
|
7ad1c4649c | ||
|
|
011b15c8d9 | ||
|
|
d28af907ec | ||
|
|
1b076ccea9 | ||
|
|
818896c734 | ||
|
|
3b6225d5f0 | ||
|
|
041a6cb768 | ||
|
|
acb6de85ad | ||
|
|
cd628b4f3d | ||
|
|
19d939a074 | ||
|
|
aeaa7ef76c | ||
|
|
7659e9831a | ||
|
|
22a416ebd8 | ||
|
|
dcc9749967 | ||
|
|
7f9ba66e38 | ||
|
|
f260379fa2 | ||
|
|
544c39168b | ||
|
|
eb9aa34323 | ||
|
|
e9df0f17db | ||
|
|
bf2ae51fb0 | ||
|
|
dc20fe5916 | ||
|
|
1a6ed38f8f | ||
|
|
d23d5e3806 | ||
|
|
365a4f2888 | ||
|
|
e270c886db | ||
|
|
18a1b0e94c | ||
|
|
3799ec648d | ||
|
|
8ff06f6d29 | ||
|
|
6ff1ef932c | ||
|
|
9e29135376 | ||
|
|
109f98db66 | ||
|
|
69201bc8b1 | ||
|
|
656fa2cfe3 | ||
|
|
1588bee5a3 | ||
|
|
75b79d0202 | ||
|
|
6d9f1af134 | ||
|
|
d38459bb8a | ||
|
|
c7954d2294 | ||
|
|
adbb8e380d | ||
|
|
4ff9e88185 | ||
|
|
27a10c58ca | ||
|
|
44b148f2a4 | ||
|
|
6434814d1d | ||
|
|
043f3e7ce4 |
@@ -2,22 +2,9 @@ root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = false
|
||||
max_line_length = 120
|
||||
tab_width = 2
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.rs]
|
||||
max_line_length = 100
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.ats,*.cts,*.mts,*.ts}]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
@@ -4,11 +4,7 @@
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
@@ -17,23 +13,8 @@
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"plugins": ["react", "@typescript-eslint"],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@typescript-eslint/ban-types": [
|
||||
"warn",
|
||||
{
|
||||
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
9
.github/workflows/backend-checks.yml
vendored
9
.github/workflows/backend-checks.yml
vendored
@@ -3,12 +3,12 @@ name: Check backend
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/backend-checks.yml"
|
||||
- "src-tauri/**"
|
||||
- '.github/workflows/backend-checks.yml'
|
||||
- 'src-tauri/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/backend-checks.yml"
|
||||
- "src-tauri/**"
|
||||
- '.github/workflows/backend-checks.yml'
|
||||
- 'src-tauri/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}-${{ github.workflow }}
|
||||
@@ -59,4 +59,3 @@ jobs:
|
||||
name: clippy (${{ runner.os }})
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --manifest-path ./src-tauri/Cargo.toml --no-default-features -- -D warnings
|
||||
|
||||
|
||||
27
.github/workflows/frontend-checks.yml
vendored
27
.github/workflows/frontend-checks.yml
vendored
@@ -2,36 +2,31 @@ name: Check frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/frontend-checks.yml"
|
||||
- "src/**"
|
||||
- ".eslintrc.json"
|
||||
- "package.json"
|
||||
- "tsconfig.json"
|
||||
- "yarn.lock"
|
||||
paths-ignore:
|
||||
- '**.lock'
|
||||
- '**.rs'
|
||||
- '**.toml'
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/frontend-checks.yml"
|
||||
- "src/**"
|
||||
- ".eslintrc.json"
|
||||
- "package.json"
|
||||
- "tsconfig.json"
|
||||
- "yarn.lock"
|
||||
paths-ignore:
|
||||
- '**.lock'
|
||||
- '**.rs'
|
||||
- '**.toml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}-${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tsc-eslint-checks:
|
||||
prettier-tsc-eslint-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install modules
|
||||
run: yarn
|
||||
- name: Run prettier
|
||||
run: yarn prettier --check .
|
||||
- name: Run tsc
|
||||
run: yarn tsc --noEmit
|
||||
- name: Run ESLint
|
||||
run: yarn eslint src
|
||||
|
||||
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
||||
4
.lintstagedrc.json
Normal file
4
.lintstagedrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"src-tauri/**/*.rs": "rustfmt --edition 2021",
|
||||
"*": "yarn prettier --write --ignore-unknown"
|
||||
}
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
src-tauri/resources/
|
||||
src-tauri/target/
|
||||
src-tauri/WixTools/
|
||||
4
.prettierrc.json
Normal file
4
.prettierrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
74
README.md
74
README.md
@@ -1,21 +1,28 @@
|
||||
# Cient Patching Notice
|
||||
# Client Patching Notice
|
||||
|
||||
For game versions 2.8 and above, Cultivation automatically makes a small patch to your game client when launching using Grasscutter, and restores it upon closing the game. In theory, you should still be totally safe, however it would be dishonest to not explicitly state that **modifying the game client could, theoretically, lead to a ban if you connect to official servers with it**. It is extremely unlikely AND there are no instances known of it happening, but the possibility exists.
|
||||
|
||||
# Cultivation
|
||||
|
||||
A game launcher designed to easily proxy traffic from anime game to private servers.
|
||||
|
||||
While the Cultivation repository is **open**. This does **not** mean it has released.
|
||||
Please do **NOT install, download, or use pre-compiled versions of Cultivation found elsewhere**. Only use releases from this GitHub repository.
|
||||
|
||||
# Table Of Contents
|
||||
* [Download](#download)
|
||||
* [Developer Quick-start](#developer-quickstart)
|
||||
* [Setup](#setup)
|
||||
* [Building](#building)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
* [Theming](#theming)
|
||||
|
||||
- [Download](#download)
|
||||
- [Developer Quick-start](#developer-quickstart)
|
||||
- [Setup](#setup)
|
||||
- [Building](#building)
|
||||
- [Code Formatting and Linting](#code-formatting-and-linting)
|
||||
- [Generating Update Artifacts](#generating-update-artifacts)
|
||||
- [Theming](#theming)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Credits](#credits)
|
||||
|
||||
# Download
|
||||
|
||||
[Find release builds here!](https://github.com/Grasscutters/Cultivation/releases)
|
||||
|
||||
Once downloaded, extract somewhere and open as administrator.
|
||||
@@ -23,35 +30,47 @@ Once downloaded, extract somewhere and open as administrator.
|
||||
# Developer Quickstart
|
||||
|
||||
### Setup
|
||||
* Install [NodeJS >12](https://nodejs.org/en/)
|
||||
* Install [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#debian-stable) (cry about it `npm` lovers)
|
||||
* Install [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) & [Rust compiler](https://www.rust-lang.org/tools/install)
|
||||
* `yarn install`
|
||||
* `yarn start:dev`
|
||||
|
||||
- Install [NodeJS >12](https://nodejs.org/en/)
|
||||
- Install [yarn](https://classic.yarnpkg.com/lang/en/docs/install) (cry about it `npm` lovers)
|
||||
- Install [Rust](https://www.rust-lang.org/tools/install)
|
||||
- `yarn install`
|
||||
- `yarn start:dev`
|
||||
|
||||
### Building
|
||||
`npm run build` or `yarn build`
|
||||
|
||||
Add `--release` or `--debug` depending on what release you are creating. This defaults to `--release`
|
||||
For a release build,
|
||||
|
||||
- `yarn build`
|
||||
|
||||
For a debug build,
|
||||
|
||||
- `yarn build --debug`
|
||||
|
||||
### Code Formatting and Linting
|
||||
|
||||
Format the code with `npm format` or `yarn format`. Run the lints with `npm lint` or `yarn lint`.
|
||||
Formatting:
|
||||
|
||||
### Updating
|
||||
* Add the `TAURI_PRIVATE_KEY` as an environment variable with a path to your private key.
|
||||
* Add the `TAURI_KEY_PASSWORD` as an environment variable with the password for your private key.
|
||||
* Run `npm run update` or `yarn build`
|
||||
* The update will be in `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x64_xx-XX.msi.zip`
|
||||
- `yarn format`
|
||||
|
||||
# Troubleshooting
|
||||
TODO. Collect common issues before updating.
|
||||
Check Lints, fix (some) lints:
|
||||
|
||||
- `yarn lint`, `yarn lint:fix`
|
||||
|
||||
### Generating Update Artifacts
|
||||
|
||||
- Add the `TAURI_PRIVATE_KEY` as an environment variable with a path to your private key.
|
||||
- Add the `TAURI_KEY_PASSWORD` as an environment variable with the password for your private key.
|
||||
- `yarn build`
|
||||
|
||||
The update will be at `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x64_xx-XX.msi.zip`
|
||||
|
||||
# Theming
|
||||
|
||||
A full theming reference can be found [here!](/THEMES.md)
|
||||
|
||||
# Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||
@@ -59,8 +78,9 @@ A full theming reference can be found [here!](/THEMES.md)
|
||||

|
||||
|
||||
## Credits
|
||||
* [SpikeHD](https://github.com/SpikeHD): For originally creating **GrassClipper** and creating the amazing UI of Cultivation.
|
||||
* [KingRainbow44](https://github.com/KingRainbow44): For building a proxy daemon from scratch and integrating it with Cultivation.
|
||||
* [Benj](https://github.com/4Benj): For assistance in client patching.
|
||||
* [lilmayofuksu](https://github.com/lilmayofuksu): For assistance in client patching.
|
||||
* [Tauri](https://tauri.app): For providing an amazing, efficient, and simple desktop application framework/library.
|
||||
|
||||
- [SpikeHD](https://github.com/SpikeHD): For originally creating **GrassClipper** and creating the amazing UI of Cultivation.
|
||||
- [KingRainbow44](https://github.com/KingRainbow44): For building a proxy daemon from scratch and integrating it with Cultivation.
|
||||
- [Benj](https://github.com/4Benj): For assistance in client patching.
|
||||
- [lilmayofuksu](https://github.com/lilmayofuksu): For assistance in client patching.
|
||||
- [Tauri](https://tauri.app): For providing an amazing, efficient, and simple desktop application framework/library.
|
||||
|
||||
29
THEMES.md
29
THEMES.md
@@ -2,7 +2,7 @@
|
||||
|
||||
1. Download your favorite theme! (You can find some in the `#themes` channel on Discord)
|
||||
2. Place the unzipped theme folder inside of `%appdata%/cultivation/themes` (The path should look something like this: `cultivation/themes/theme_name/index.json`)
|
||||
4. Enable within Cultivation!
|
||||
3. Enable within Cultivation!
|
||||
|
||||
# Creating your own theme
|
||||
|
||||
@@ -17,7 +17,7 @@ You will need CSS and JS experience if you want to do anything cool.
|
||||
`index.json` is where you tell Cultivation which files and images to include. It supports the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| :--- | :--- |
|
||||
| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | The name of the theme. |
|
||||
| `version` | Not shown anywhere, the version of the theme. |
|
||||
| `description` | Not shown anywhere, the description of the theme. |
|
||||
@@ -55,15 +55,17 @@ Below are some small examples of what you can do:
|
||||
```css
|
||||
/* Change the font */
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* Remove the news section */
|
||||
.NewsSection {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* Change the right bar width */
|
||||
.RightBar {
|
||||
@@ -72,6 +74,7 @@ body {
|
||||
```
|
||||
|
||||
## How can I change XYZ element?
|
||||
|
||||
Every element is documented and describe [here](/docs/elementIds.md). Every\* single DOM element is assigned an ID to allow for easy and hyper-specific editing.
|
||||
|
||||
## Writing your JS
|
||||
@@ -83,24 +86,26 @@ Below are some examples of what you can do:
|
||||
```js
|
||||
/* Change the version number every 500ms */
|
||||
setInterval(() => {
|
||||
document.getElementById("version").innerHTML = "v" + Math.floor(Math.random() * 100);
|
||||
}, 500);
|
||||
document.getElementById('version').innerHTML = 'v' + Math.floor(Math.random() * 100)
|
||||
}, 500)
|
||||
```
|
||||
|
||||
```js
|
||||
/* Load a custom font */
|
||||
const head = document.head
|
||||
const link = document.createElement("link")
|
||||
const link = document.createElement('link')
|
||||
|
||||
link.href = "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"
|
||||
link.rel = "stylesheet"
|
||||
link.type = "text/css"
|
||||
link.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
|
||||
head.appendChild(link)
|
||||
```
|
||||
|
||||
```js
|
||||
/* Create a new button that does nothing */
|
||||
const newButton = document.createElement("button");
|
||||
newButton.innerHTML = "New Button";
|
||||
const newButton = document.createElement('button')
|
||||
newButton.innerHTML = 'New Button'
|
||||
|
||||
document.body.appendChild(newButton);
|
||||
document.body.appendChild(newButton)
|
||||
```
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Documentation of Element ID's and Classes for custom theming
|
||||
|
||||
## IDs
|
||||
|
||||
This does not include commonly used components (buttons, divider lines, commit author and message, etc...) for accessing and modifying those elements, please check `Classes` section bellow.
|
||||
|
||||
| #ID | Description |
|
||||
|----------------------------------------|-----------------------------------------------------------------|
|
||||
| ------------------------------------ | --------------------------------------------------------------- |
|
||||
| `#miniDialogContainer` | Main container of MiniDialog |
|
||||
| `#miniDialogContainerTop` | Affects only top section of MiniDialog |
|
||||
| `#miniDialogButtonClose` | Close button (SVG) of MiniDialog |
|
||||
@@ -97,12 +98,13 @@ This does not include commonly used components (buttons, divider lines, commit a
|
||||
| `#miniDownloadContainer` | Container for mini download |
|
||||
|
||||
## Classes
|
||||
|
||||
This is not full list of all classes, rather its list of classes for commonly used components that can not be accessed using element id system.
|
||||
|
||||
| .Class | Description |
|
||||
|-----------------------------|---------------------------------------------------------|
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| `.BigButton` | Class for all buttons |
|
||||
| `.BigButtonText` | Text inside a button | |
|
||||
| `.BigButtonText` | Text inside a button |
|
||||
| `.Checkbox` | Checkbox container |
|
||||
| `.CheckboxDisplay` | Content of checkbox |
|
||||
| `.DirInput` | Container for DirInput |
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
# Troubleshooting
|
||||
|
||||
A guide dedicated for trying to troubleshoot Cultivation.
|
||||
|
||||
## The launcher doesn't appear to open.
|
||||
|
||||
Try running the launcher with **administrative privileges**.\
|
||||
If this fixes your issue, you can force enable it in the **Compatability**\
|
||||
tab for the launcher's executable.
|
||||
|
||||
## Unable to play on `localhost`.
|
||||
|
||||
Make sure your server is running with **encryption disabled** and `useInRouting` to **false**.\
|
||||
Additionally, make sure Cultivation **is set to not use HTTPS**.
|
||||
|
||||
## "I can't do anything requiring the internet after closing Cultivation!"
|
||||
|
||||
You probably didn't close Cultivation properly.\
|
||||
Go to your *Windows Settings*, then *Network*, then *Proxy*, then disable it.
|
||||
Go to your _Windows Settings_, then _Network_, then _Proxy_, then disable it.
|
||||
|
||||
18
package.json
18
package.json
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "cultivation",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.6",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.0.0-rc.5",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@testing-library/user-event": "^14.2.6",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/node": "^18.0.6",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"react": "^18.1.0",
|
||||
@@ -28,8 +28,10 @@
|
||||
"eject": "react-scripts eject",
|
||||
"tauri": "tauri",
|
||||
"start:dev": "tauri dev",
|
||||
"format": "cargo fmt --manifest-path ./src-tauri/Cargo.toml --all",
|
||||
"lint": "cargo clippy --manifest-path ./src-tauri/Cargo.toml --no-default-features && yarn tsc --noEmit && yarn eslint src"
|
||||
"format": "cargo fmt --manifest-path ./src-tauri/Cargo.toml --all && yarn prettier --write --cache --loglevel warn .",
|
||||
"lint": "cargo clippy --manifest-path ./src-tauri/Cargo.toml --no-default-features && yarn tsc --noEmit && yarn eslint src",
|
||||
"lint:fix": "cargo clippy --manifest-path ./src-tauri/Cargo.toml --no-default-features --fix --allow-dirty && yarn tsc --noEmit && yarn eslint --fix src",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -55,7 +57,11 @@
|
||||
"@typescript-eslint/parser": "^5.22.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.15.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"run-script-os": "^1.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Tauri-powered anime game launcher"
|
||||
/>
|
||||
<meta name="description" content="Tauri-powered anime game launcher" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Cultivation</title>
|
||||
|
||||
318
src-tauri/Cargo.lock
generated
318
src-tauri/Cargo.lock
generated
@@ -75,7 +75,7 @@ dependencies = [
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
"rusticata-macros",
|
||||
"thiserror",
|
||||
"time 0.3.11",
|
||||
@@ -506,9 +506,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
|
||||
checksum = "83827793632c72fa4f73c2edb31e7a997527dd8ffe7077344621fc62c5478157"
|
||||
dependencies = [
|
||||
"cache-padded",
|
||||
]
|
||||
@@ -657,9 +657,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
@@ -719,9 +719,8 @@ dependencies = [
|
||||
"http",
|
||||
"hudsucker",
|
||||
"is_elevated",
|
||||
"libloading",
|
||||
"once_cell",
|
||||
"open 2.1.3",
|
||||
"open",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"registry",
|
||||
@@ -737,6 +736,7 @@ dependencies = [
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"unrar",
|
||||
"zip 0.6.2",
|
||||
"zip-extract",
|
||||
]
|
||||
@@ -784,9 +784,9 @@ checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce"
|
||||
checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
@@ -821,8 +821,8 @@ dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"num-bigint 0.4.3",
|
||||
"num-traits 0.2.15",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
@@ -949,6 +949,15 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum_primitive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180"
|
||||
dependencies = [
|
||||
"num-traits 0.1.43",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-chain"
|
||||
version = "0.12.4"
|
||||
@@ -1042,6 +1051,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
@@ -1239,15 +1254,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee"
|
||||
checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"winapi",
|
||||
"windows 0.32.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1463,9 +1478,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -1700,8 +1715,8 @@ dependencies = [
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"num-rational 0.4.1",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1887,16 +1902,6 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line-wrap"
|
||||
version = "0.1.1"
|
||||
@@ -1948,9 +1953,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.5.2"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "042f74a606175d72ca483e14e0873fe0f6c003f7af45865b17b16fdaface7203"
|
||||
checksum = "fff231a88fe2e9985f9d159a2f02986fe46daa0f6af976a0d934be4870cc9d02"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dirs-next",
|
||||
@@ -2182,6 +2187,32 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.44",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational 0.1.42",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits 0.2.15",
|
||||
"rand 0.4.6",
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.3"
|
||||
@@ -2190,7 +2221,17 @@ checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656"
|
||||
dependencies = [
|
||||
"num-traits 0.2.15",
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2200,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2211,7 +2252,19 @@ checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.44",
|
||||
"num-integer",
|
||||
"num-traits 0.2.15",
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2222,7 +2275,16 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||
dependencies = [
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2336,19 +2398,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "2.1.3"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2423ffbf445b82e58c3b1543655968923dd06f85432f10be2bb4f1b7122f98c"
|
||||
dependencies = [
|
||||
"pathdiff",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "360bcc8316bf6363aa3954c3ccc4de8add167b087e0259190a043c9514f910fe"
|
||||
checksum = "f23a407004a1033f53e93f9b45580d14de23928faad187384f891507c9b0c045"
|
||||
dependencies = [
|
||||
"pathdiff",
|
||||
"windows-sys",
|
||||
@@ -2356,9 +2408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.40"
|
||||
version = "0.10.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
|
||||
checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -2388,9 +2440,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.74"
|
||||
version = "0.9.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
|
||||
checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
@@ -2546,9 +2598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947"
|
||||
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
|
||||
dependencies = [
|
||||
"base64",
|
||||
]
|
||||
@@ -2852,6 +2904,19 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
||||
dependencies = [
|
||||
"fuchsia-cprng",
|
||||
"libc",
|
||||
"rand_core 0.3.1",
|
||||
"rdrand",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
@@ -2897,6 +2962,21 @@ dependencies = [
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
@@ -2977,9 +3057,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7fa2d386df8533b02184941c76ae2e0d0c1d053f5d43339169d80f21275fc5e"
|
||||
checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
@@ -2988,6 +3068,15 @@ dependencies = [
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
@@ -3134,6 +3223,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.3.3"
|
||||
@@ -3184,9 +3279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf"
|
||||
checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -3322,18 +3417,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.138"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
|
||||
checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.138"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
|
||||
checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3789,9 +3884,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d61fc211e0bd2c04c0aecd202d2cd72dd797a89da02989a39e1b9691462386d6"
|
||||
checksum = "827f61bd3dd40276694be5c7ffc40d65b94ab00d9f8c1a4a4db07f2cdc306c83"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc",
|
||||
@@ -3810,7 +3905,7 @@ dependencies = [
|
||||
"notify-rust",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"open 3.0.1",
|
||||
"open",
|
||||
"os_info",
|
||||
"os_pipe 1.0.1",
|
||||
"percent-encoding",
|
||||
@@ -3842,9 +3937,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f2b32e551ec810ba4ab2ad735de5e3576e54bf0322ab0f4b7ce41244bc65ecf"
|
||||
checksum = "acafb1c515c5d14234a294461bd43c723639a84891a45f6a250fd3441ad2e8ed"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -3858,9 +3953,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f1f7928dd040fc03c94207adfad506c0cf5b152982fd1dc0a621f7fd777e22"
|
||||
checksum = "16d62a3c8790d6cba686cea6e3f7f569d12c662c3274c2d165a4fd33e3871b72"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"brotli",
|
||||
@@ -3884,9 +3979,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e50b9f52871c088857360319a37472d59f4644f1ed004489599d62831a1b6996"
|
||||
checksum = "7296fa17996629f43081e1c66d554703900187ed900c5bf46f97f0bcfb069278"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro2",
|
||||
@@ -4068,10 +4163,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.19.2"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
|
||||
checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
@@ -4107,9 +4203,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.17.1"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06cda1232a49558c46f8a504d5b93101d42c0bf7f911f12a105ba48168f821ae"
|
||||
checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -4236,9 +4332,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.17.2"
|
||||
version = "0.17.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
|
||||
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"byteorder",
|
||||
@@ -4284,9 +4380,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
|
||||
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -4309,6 +4405,31 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
||||
|
||||
[[package]]
|
||||
name = "unrar"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433cea4f0b7bec88d47becb380887b8786a3cfb1c82e1ef9d32a682ba6801814"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"enum_primitive",
|
||||
"lazy_static",
|
||||
"num",
|
||||
"regex",
|
||||
"unrar_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unrar_sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0009399408dc0bcc5c8910672544fceceeba18b91f741ff943916e917d982c60"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
@@ -4690,6 +4811,19 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.24.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc 0.32.0",
|
||||
"windows_i686_gnu 0.32.0",
|
||||
"windows_i686_msvc 0.32.0",
|
||||
"windows_x86_64_gnu 0.32.0",
|
||||
"windows_x86_64_msvc 0.32.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.37.0"
|
||||
@@ -4749,6 +4883,12 @@ version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
@@ -4767,6 +4907,12 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0866510a3eca9aed73a077490bbbf03e5eaac4e1fd70849d89539e5830501fd"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
@@ -4785,6 +4931,12 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf0ffed56b7e9369a29078d2ab3aaeceea48eb58999d2cff3aa2494a275b95c6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
@@ -4803,6 +4955,12 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384a173630588044205a2993b6864a2f56e5a8c1e7668c07b93ec18cf4888dc4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
@@ -4821,6 +4979,12 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd8f062d8ca5446358159d79a90be12c543b3a965c847c8f3eedf14b321d399"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
|
||||
@@ -31,13 +31,14 @@ sysinfo = "0.24.6"
|
||||
|
||||
# ZIP-archive library.
|
||||
zip-extract = "0.1.1"
|
||||
unrar = "0.4.4"
|
||||
zip = "0.6.2"
|
||||
|
||||
# For creating a "global" downloads list.
|
||||
once_cell = "1.13.0"
|
||||
|
||||
# Program opener.
|
||||
open = "2.1.2"
|
||||
open = "3.0.2"
|
||||
duct = "0.13.5"
|
||||
|
||||
# Serialization.
|
||||
@@ -56,7 +57,6 @@ futures-util = "0.3.14"
|
||||
rcgen = { version = "0.9", features = ["x509-parser"] }
|
||||
|
||||
# metadata stuff
|
||||
libloading = "0.7"
|
||||
regex = "1"
|
||||
|
||||
# other
|
||||
|
||||
@@ -2,17 +2,14 @@ fn main() {
|
||||
cc::Build::new()
|
||||
.include("mhycrypto")
|
||||
.cpp(true)
|
||||
|
||||
.file("mhycrypto/memecrypto.cpp")
|
||||
.file("mhycrypto/metadata.cpp")
|
||||
.file("mhycrypto/metadatastringdec.cpp")
|
||||
|
||||
.compile("mhycrypto");
|
||||
|
||||
cc::Build::new()
|
||||
.include("mhycrypto")
|
||||
.file("mhycrypto/aes.c")
|
||||
|
||||
.compile("mhycrypto-aes");
|
||||
|
||||
tauri_build::build()
|
||||
|
||||
BIN
src-tauri/icons/icon_resize.png
Normal file
BIN
src-tauri/icons/icon_resize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -11,13 +11,21 @@
|
||||
"files_extracting": "文件解压中:"
|
||||
},
|
||||
"options": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"game_path": "选择游戏安装路径",
|
||||
"game_executable": "选择游戏可执行文件",
|
||||
"recover_metadata": "紧急情况下恢复元数据文件",
|
||||
"grasscutter_jar": "选择 Grasscutter JAR 文件",
|
||||
"java_path": "设置自定义 Java 路径",
|
||||
"toggle_encryption": "启用加密",
|
||||
"install_certificate": "安装代理证书",
|
||||
"java_path": "选择自定义 Java 路径",
|
||||
"grasscutter_with_game": "随游戏自动启动 Grasscutter",
|
||||
"language": "语言",
|
||||
"language": "选择语言",
|
||||
"background": "设置自定义背景(链接或文件)",
|
||||
"theme": "设置主题"
|
||||
"theme": "设置主题",
|
||||
"patch_metadata": "自动修改元数据",
|
||||
"use_proxy": "使用内置代理"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "下载 Grasscutter 稳定版数据",
|
||||
@@ -28,7 +36,8 @@
|
||||
"grasscutter_latest": "下载 Grasscutter 开发版",
|
||||
"grasscutter_stable_update": "更新 Grasscutter 稳定版",
|
||||
"grasscutter_latest_update": "更新 Grasscutter 开发版",
|
||||
"resources": "下载 Grasscutter 资源"
|
||||
"resources": "下载 Grasscutter 资源",
|
||||
"game": "下载游戏"
|
||||
},
|
||||
"download_status": {
|
||||
"downloading": "下载中",
|
||||
@@ -40,10 +49,11 @@
|
||||
"components": {
|
||||
"select_file": "选择文件或文件夹...",
|
||||
"select_folder": "选择文件夹...",
|
||||
"download": "下载"
|
||||
"download": "下载",
|
||||
"install": "安装"
|
||||
},
|
||||
"news": {
|
||||
"latest_commits": "最近的PR",
|
||||
"latest_commits": "最近提交",
|
||||
"latest_version": "最新版本"
|
||||
},
|
||||
"help": {
|
||||
@@ -53,6 +63,17 @@
|
||||
"gc_dev_jar": "下载最新的 Grasscutter 开发版,包括 JAR 文件和数据。",
|
||||
"gc_stable_data": "下载当前的 Grasscutter 稳定版数据,不包括 JAR 文件。此选项在更新时有帮助。",
|
||||
"gc_dev_data": "下载最新的 Grasscutter 开发版数据,不包括 JAR 文件。此选项在更新时有帮助。",
|
||||
"resources": "资源文件在运行 Grasscutter 服务器时是必要的。此选项在已经存在资源文件时不可选。"
|
||||
"resources": "资源文件在运行 Grasscutter 服务器时是必要的。此选项在已经存在资源文件时不可选。",
|
||||
"emergency_metadata": "在出现意外情况时,自动将元数据恢复至原始版本",
|
||||
"use_proxy": "使用 Cultivation 的内置代理。除非你使用 Fiddler 等软件,否则应启用此项。",
|
||||
"patch_metadata": "自动修改和恢复游戏元数据。除非要游玩旧版本/非官方版本,抑或你已经手动修改了元数据,否则应启用此项。"
|
||||
},
|
||||
"swag": {
|
||||
"akebi_name": "Akebi",
|
||||
"migoto_name": "Migoto",
|
||||
"reshade_name": "Reshade",
|
||||
"akebi": "选择 Akebi 可执行文件",
|
||||
"migoto": "选择 3DMigoto 可执行文件",
|
||||
"reshade": "选择 Reshade 注入器"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,19 @@
|
||||
"options": {
|
||||
"enabled": "已啟用",
|
||||
"disabled": "未啟用",
|
||||
"game_path": "選擇遊戲安裝路徑",
|
||||
"game_executable": "選擇遊戲執行檔",
|
||||
"recover_metadata": "緊急恢復Metadata",
|
||||
"grasscutter_jar": "選擇伺服器JAR檔案",
|
||||
"toggle_encryption": "設定加密",
|
||||
"java_path": "設定自定義Java路徑",
|
||||
"install_certificate": "安裝代理憑證",
|
||||
"java_path": "選擇自定義Java路徑",
|
||||
"grasscutter_with_game": "伴隨遊戲一起啟動Grasscutter",
|
||||
"language": "語言",
|
||||
"background": "設定自定義背景(網址或檔案)",
|
||||
"theme": "設定主題"
|
||||
"background": "選擇自定義背景(網址或檔案)",
|
||||
"theme": "選擇主題",
|
||||
"patch_metadata": "自動修補Metadata",
|
||||
"use_proxy": "使用內建代理伺服器"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "下載Grasscutter穩定版數據(Data)",
|
||||
@@ -31,7 +36,8 @@
|
||||
"grasscutter_latest": "下載Grasscutter開發板",
|
||||
"grasscutter_stable_update": "更新Grasscutter穩定版",
|
||||
"grasscutter_latest_update": "更新Grasscutter開發板",
|
||||
"resources": "下載Grasscutter資源(Resources)"
|
||||
"resources": "下載Grasscutter資源(Resources)",
|
||||
"game": "下載遊戲"
|
||||
},
|
||||
"download_status": {
|
||||
"downloading": "下載中",
|
||||
@@ -43,7 +49,8 @@
|
||||
"components": {
|
||||
"select_file": "選擇檔案或資料夾...",
|
||||
"select_folder": "選擇資料夾...",
|
||||
"download": "下載"
|
||||
"download": "下載",
|
||||
"install": "安裝"
|
||||
},
|
||||
"news": {
|
||||
"latest_commits": "最近的PR",
|
||||
@@ -51,11 +58,23 @@
|
||||
},
|
||||
"help": {
|
||||
"port_help_text": "確保這是Dispatch伺服器端口,不是遊戲伺服器端口。 大部分伺服器的端口都是443。",
|
||||
"game_help_text": "你不需要另外一個遊戲備份來使用Grasscutter。這是給想要降級到2.6或者還沒安裝遊戲的人使用的。",
|
||||
"game_help_text": "您不需要另外一個遊戲備份來使用Grasscutter。這是給想要降級到2.6或者還沒安裝遊戲的人使用的。",
|
||||
"gc_stable_jar": "下載當前的Grasscutter穩定版本,包括JAR答案還有資料文件。",
|
||||
"gc_dev_jar": "下載當前的Grasscutter穩定版本資料文件,其中不會附帶JAR文件。這個選項在更新時很有用。",
|
||||
"gc_stable_data": "下載當前最新的Grasscutter開發版本資料文件,其中不會附帶JAR文件。這個選項在更新時很有用。",
|
||||
"gc_dev_data": "下載當前最新的Grasscutter開發版本的資料文件,其中不會附帶JAR文件。這個選項在更新時很有用。",
|
||||
"resources": "資源文件在架設一個Grasscutter伺服器時是必要的。 這個選項會在你已經有裡面有檔案的資源資料夾時不可選。"
|
||||
"encryption": "在正常情況下,此選項應該被關閉。",
|
||||
"resources": "資源文件在架設一個Grasscutter伺服器時是必要的。 這個選項會在您已經有裡面有檔案的資源資料夾時不可選。",
|
||||
"emergency_metadata": "一旦有東西出了問題,此選項可以把您的Metadata恢復成官方版本。",
|
||||
"use_proxy": "使用Cultivation內建的代理伺服器。此選項應該被啟用,除非你使用其他的代理伺服器。",
|
||||
"patch_metadata": "自動修補和恢復Metadata。除非您的遊戲版本是舊的或者是非官方的,此選項應該被啟用。"
|
||||
},
|
||||
"swag": {
|
||||
"akebi_name": "Akebi",
|
||||
"migoto_name": "Migoto",
|
||||
"reshade_name": "Reshade",
|
||||
"akebi": "選擇Akebi執行檔",
|
||||
"migoto": "選擇3DMigoto執行檔",
|
||||
"reshade": "選擇Reshade注入器"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,17 @@
|
||||
"options": {
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"game_path": "Spielpfad",
|
||||
"game_executable": "Spiel Datei auswählen",
|
||||
"recover_metadata": "Notfall Wiederherstellung der Metadaten",
|
||||
"grasscutter_jar": "Grasscuter JAR auswählen",
|
||||
"toggle_encryption": "Verschlüsselung umschalten",
|
||||
"java_path": "Benutzerdefinierten Java Pfad setzen",
|
||||
"grasscutter_with_game": "Grasscutter automatisch mit dem Spiel starten",
|
||||
"language": "Sprache auswählen",
|
||||
"background": "Benutzerdefinierten Hintergrund festlegen (link oder bild)",
|
||||
"theme": "Theme auswählen"
|
||||
"theme": "Theme auswählen",
|
||||
"patch_metadata": "Metadaten automatisch patchen"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "Stabile Grasscutter Daten herunterladen",
|
||||
@@ -31,7 +34,8 @@
|
||||
"grasscutter_latest": "Aktuellste Grasscutter Version herunterladen",
|
||||
"grasscutter_stable_update": "Stabile Grasscutter Version aktualisieren",
|
||||
"grasscutter_latest_update": "Aktuellste Grasscutter Version aktualisieren",
|
||||
"resources": "Grasscutter Ressourcen herunterladen"
|
||||
"resources": "Grasscutter Ressourcen herunterladen",
|
||||
"game": "Spiel herunterladen"
|
||||
},
|
||||
"download_status": {
|
||||
"downloading": "Lädt herunter",
|
||||
@@ -43,7 +47,8 @@
|
||||
"components": {
|
||||
"select_file": "Datei oder Ordner auswählen...",
|
||||
"select_folder": "Ordner auswählen...",
|
||||
"download": "Herunterladen"
|
||||
"download": "Herunterladen",
|
||||
"install": "Installieren"
|
||||
},
|
||||
"news": {
|
||||
"latest_commits": "Letzte Commits",
|
||||
@@ -56,6 +61,17 @@
|
||||
"gc_dev_jar": "Laden Sie die neueste Grasscutter-Entwicklungsversion herunter, welche eine Jar-Datei und Datendateien enthält.",
|
||||
"gc_stable_data": "Laden Sie die stabilen Grasscutter Daten herunter, welche keine Jar-Datei enthalten. Dies ist nützlich zum Aktualisieren.",
|
||||
"gc_dev_data": "Laden Sie die neuesten Grasscutter-Entwicklungsdateien herunter, welche keine Jar-Datei enthält. Dies ist nützlich zum Aktualisieren.",
|
||||
"resources": "Diese werden auch benötigt, um einen Grasscutter-Server auszuführen. Diese Schaltfläche ist grau, wenn Sie einen bestehenden Ressourcenordner mit Inhalten haben"
|
||||
}
|
||||
"resources": "Diese werden auch benötigt, um einen Grasscutter-Server auszuführen. Diese Schaltfläche ist grau, wenn Sie einen bestehenden Ressourcenordner mit Inhalten haben",
|
||||
"emergency_metadata": "Im Fall, dass etwas schief laufen sollte, kannst du deine Metadaten auf die letzte offizielle Version zurücksetzen",
|
||||
"use_proxy": "Nutze den internen Proxy von Cultivation. Du solltest dies aktivieren, es sei denn du nutzt Programme wie Fiddler",
|
||||
"patch_metadata": "Patche und aktualisiere deine Metadaten automatisch. Solange du nicht mit einer alten/nicht offiziellen Version spielst oder deine Metadaten manuell gepatcht hast, sollte dies aktiviert sein."
|
||||
},
|
||||
"swag": {
|
||||
"akebi_name": "Akebi",
|
||||
"migoto_name": "Migoto",
|
||||
"reshade_name": "Reshade",
|
||||
"akebi": "Akebi.exe festlegen",
|
||||
"migoto": "Migoto.exe festlegen",
|
||||
"reshade": "Reshade injector festlegen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
"grasscutter_with_game": "Automatically launch Grasscutter with game",
|
||||
"language": "Select Language",
|
||||
"background": "Set Custom Background (link or image file)",
|
||||
"theme": "Set Theme"
|
||||
"theme": "Set Theme",
|
||||
"patch_metadata": "Automatically Patch Metadata",
|
||||
"use_proxy": "Use Internal Proxy"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "Download Grasscutter Stable Data",
|
||||
@@ -61,9 +63,18 @@
|
||||
"gc_dev_jar": "Download the latest development Grasscutter build, which includes jar file and data files.",
|
||||
"gc_stable_data": "Download the current stable Grasscutter data files, which does not come with a jar file. This is useful for updating.",
|
||||
"gc_dev_data": "Download the latest development Grasscutter data files, which does not come with a jar file. This is useful for updating.",
|
||||
"resources": "These are also required to run a Grasscutter server. This button will be grey if you have an existing resources folder with contents inside"
|
||||
"encryption": "This should usually be disabled.",
|
||||
"resources": "These are also required to run a Grasscutter server. This button will be grey if you have an existing resources folder with contents inside",
|
||||
"emergency_metadata": "In case something went wrong, restore your metadata to the latest official versions metadata.",
|
||||
"use_proxy": "Use the Cultivation internal proxy. You should have this enabled unless you use something like Fiddler",
|
||||
"patch_metadata": "Patch and unpatch your game metadata automatically. Unless playing with old/non-official versions, or you have manually patched your metadata, this should be enabled."
|
||||
},
|
||||
"swag": {
|
||||
"akebi": "Set Akebi Executable"
|
||||
"akebi_name": "Akebi",
|
||||
"migoto_name": "Migoto",
|
||||
"reshade_name": "Reshade",
|
||||
"akebi": "Set Akebi Executable",
|
||||
"migoto": "Set 3DMigoto Executable",
|
||||
"reshade": "Set Reshade Injector"
|
||||
}
|
||||
}
|
||||
69
src-tauri/lang/es.json
Normal file
69
src-tauri/lang/es.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"lang_name": "Español",
|
||||
"main": {
|
||||
"title": "Cultivation",
|
||||
"launch_button": "Launch",
|
||||
"gc_enable": "Conectar Via Grasscutter",
|
||||
"https_enable": "Usar HTTPS",
|
||||
"ip_placeholder": "Dirección del servidor...",
|
||||
"port_placeholder": "Puerto...",
|
||||
"files_downloading": "Archivos Descargandose: ",
|
||||
"files_extracting": "Archivos Extrayendose: "
|
||||
},
|
||||
"options": {
|
||||
"enabled": "Activado",
|
||||
"disabled": "Desactivado",
|
||||
"game_path": "Ruta de instalación del juego",
|
||||
"game_executable": "Establecer ejecutable del juego",
|
||||
"recover_metadata": "Recuperación de Metadatos de Emergencia",
|
||||
"grasscutter_jar": "Establecer JAR de Grasscutter",
|
||||
"toggle_encryption": "Alternar Cifrado",
|
||||
"install_certificate": "Instalar Certificado Proxie",
|
||||
"java_path": "Establecer Ruta Personalizada de Java",
|
||||
"grasscutter_with_game": "Iniciar automáticamente Grasscutter con el juego",
|
||||
"language": "Seleccionar Idioma",
|
||||
"background": "Establecer Fondo Personalizado (link o archivo de imagen)",
|
||||
"theme": "Establecer Tema"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "Descargar Datos Estables de Grasscutter",
|
||||
"grasscutter_latest_data": "Descargar Datos más Recientes de Grasscutter",
|
||||
"grasscutter_stable_data_update": "Actualizar Datos Estables de Grasscutter",
|
||||
"grasscutter_latest_data_update": "Actualizar Datos más Recientes de Grasscutter",
|
||||
"grasscutter_stable": "Descargar Grasscutter Estable",
|
||||
"grasscutter_latest": "Descargar Grasscutter más reciente",
|
||||
"grasscutter_stable_update": "Actualizar Grasscutter Estable",
|
||||
"grasscutter_latest_update": "Actualizar Grasscutter más reciente",
|
||||
"resources": "Descargar Recursos de Grasscutter",
|
||||
"game": "Descarga el juego"
|
||||
},
|
||||
"download_status": {
|
||||
"downloading": "Descargando",
|
||||
"extracting": "Extrayendo",
|
||||
"error": "Error",
|
||||
"finished": "Finalizado",
|
||||
"stopped": "Detenido"
|
||||
},
|
||||
"components": {
|
||||
"select_file": "Seleccionar el archivo o carpeta...",
|
||||
"select_folder": "Seleccionar la carpeta...",
|
||||
"download": "Descargar",
|
||||
"install": "Instalar"
|
||||
},
|
||||
"news": {
|
||||
"latest_commits": "Commits Recientes",
|
||||
"latest_version": "Ultima versión"
|
||||
},
|
||||
"help": {
|
||||
"port_help_text": "Asegúrese de que este sea el Dispatch server port, no el Game server port. Este es casi siempre '443'.",
|
||||
"game_help_text": "No necesitas usar una copia separada para jugar con Grasscutter. Esto es para cambiar a 2.6 o si no tienes el juego instalado.",
|
||||
"gc_stable_jar": "Descargue la versión Estable actual de Grasscutter, que incluye el archivo jar y los archivos de datos.",
|
||||
"gc_dev_jar": "Descargue la última versión de Desarrollo de Grasscutter, que incluye archivos jar y archivos de datos.",
|
||||
"gc_stable_data": "Descargue los archivos de Datos Estables actuales de Grasscutter, que no vienen con un archivo jar. Esto es útil para actualizar.",
|
||||
"gc_dev_data": "Descargue los últimos archivos de Datos de Desarrollo de Grasscutter, que no vienen con un archivo jar. Esto es útil para actualizar.",
|
||||
"resources": "Estos también son necesarios para ejecutar un servidor Grasscutter. Este botón estará gris si tiene una carpeta de recursos existente con contenido dentro."
|
||||
},
|
||||
"swag": {
|
||||
"akebi": "Establecer el ejecutable de Akebi"
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,19 @@
|
||||
"options": {
|
||||
"enabled": "Включено",
|
||||
"disabled": "Выключено",
|
||||
"game_path": "Установить путь к файлам игры",
|
||||
"game_executable": "Установить исполняемый файл игры",
|
||||
"recover_metadata": "Принудительное восстановление Метаданных",
|
||||
"grasscutter_jar": "Установить Grasscutter JAR",
|
||||
"toggle_encryption": "Переключить шифрование",
|
||||
"install_certificate": "Установить сертификат для работы Прокси",
|
||||
"java_path": "Установить пользовательский путь Java",
|
||||
"grasscutter_with_game": "Автоматически запускать Grasscutter вместе с игрой",
|
||||
"language": "Установить язык",
|
||||
"background": "Установить свой фон (ссылка или файл)",
|
||||
"theme": "Установить тему"
|
||||
"theme": "Установить тему",
|
||||
"patch_metadata": "Автоматический патч Метаданных при запуске",
|
||||
"use_proxy": "Использовать встроенный Прокси"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "Скачать стабильные данные Grasscutter",
|
||||
@@ -31,7 +36,8 @@
|
||||
"grasscutter_latest": "Скачать последнюю версию Grasscutter",
|
||||
"grasscutter_stable_update": "Обновить стабильную версию Grasscutter",
|
||||
"grasscutter_latest_update": "Обновить последнюю версию Grasscutter",
|
||||
"resources": "Скачать ресурсы Grasscutter"
|
||||
"resources": "Скачать ресурсы Grasscutter",
|
||||
"game": "Скачать Игру"
|
||||
},
|
||||
"download_status": {
|
||||
"downloading": "Скачивание",
|
||||
@@ -43,7 +49,8 @@
|
||||
"components": {
|
||||
"select_file": "Выберите файл или папку...",
|
||||
"select_folder": "Выберите папку...",
|
||||
"download": "Скачать"
|
||||
"download": "Скачать",
|
||||
"install": "Установить"
|
||||
},
|
||||
"news": {
|
||||
"latest_commits": "Последние коммиты",
|
||||
@@ -56,6 +63,18 @@
|
||||
"gc_dev_jar": "Скачать последнюю версию для разработки Grasscutter, которая содержит jar файл и данные.",
|
||||
"gc_stable_data": "Скачать стабильные данные Grasscutter, в которой нету jar файла. Это полезно для обновления.",
|
||||
"gc_dev_data": "Скачать последнюю версию для разработки Grasscutter, в которой нету jar файла. Это полезно для обновления.",
|
||||
"resources": "Это необходимо для запуска сервера Grasscutter. Эта кнопка будет серой, если у Вас уже есть не пустая папка с ресурсами."
|
||||
"encryption": "Обычно это должно быть выключено.",
|
||||
"resources": "Это необходимо для запуска сервера Grasscutter. Эта кнопка будет серой, если у Вас уже есть не пустая папка с ресурсами.",
|
||||
"emergency_metadata": "Если что-то пошло не так, восстановит Метаданные до последней официальной версии.",
|
||||
"use_proxy": "Использовать встроенный Прокси. Отключите если используете Fiddler или подобную программу",
|
||||
"patch_metadata": "Патчит и восстанавливает Метаданные автоматически. Если вы не играете на старых/модифицированых версиях, или сами в ручную патчите Метаданные, эта опция должна быть включена."
|
||||
},
|
||||
"swag": {
|
||||
"akebi_name": "Akebi",
|
||||
"migoto_name": "Migoto",
|
||||
"reshade_name": "Reshade",
|
||||
"akebi": "Путь к исполняемому файлу Akebi",
|
||||
"migoto": "Путь к исполняемому файлу 3DMigoto ",
|
||||
"reshade": "Путь к инжектору Reshade"
|
||||
}
|
||||
}
|
||||
@@ -3,59 +3,70 @@
|
||||
"main": {
|
||||
"title": "Cultivation",
|
||||
"launch_button": "Khởi Chạy",
|
||||
"gc_enable": "Kết nối đến Grasscutter",
|
||||
"https_enable": "Sử dụng HTTPS",
|
||||
"gc_enable": "Kết nối qua Grasscutter",
|
||||
"https_enable": "Dùng HTTPS",
|
||||
"ip_placeholder": "Địa chỉ máy chủ...",
|
||||
"port_placeholder": "Cổng...",
|
||||
"files_downloading": "Đang tải file: ",
|
||||
"files_extracting": "Đang giải nén tệp tin: "
|
||||
"files_downloading": "Đang tải tập tin: ",
|
||||
"files_extracting": "Đang giải nén tập tin: "
|
||||
},
|
||||
"options": {
|
||||
"enabled": "Bật",
|
||||
"disabled": "Tắt",
|
||||
"game_exec": "Đường dẫn đến GenshinImpact.exe",
|
||||
"grasscutter_jar": "Đường dẫn đến Grasscutter.jar",
|
||||
"toggle_encryption": "Bật/Tắt mã hoá",
|
||||
"java_path": "Đường dẫn Java tuỳ chỉnh",
|
||||
"grasscutter_with_game": "Tự động khởi chạy Grasscutter cùng game",
|
||||
"game_path": "Đường dẫn cài game",
|
||||
"game_executable": "Tập tin thực thi game",
|
||||
"recover_metadata": "Khôi phục Metadata khẩn cấp",
|
||||
"grasscutter_jar": "Tập tin JAR Grasscutter",
|
||||
"toggle_encryption": "Bật/tắt mã hóa",
|
||||
"install_certificate": "Cài chứng chỉ proxy",
|
||||
"java_path": "Đường dẫn Java tùy chỉnh",
|
||||
"grasscutter_with_game": "Tự động chạy Grasscutter cùng với game",
|
||||
"language": "Chọn ngôn ngữ",
|
||||
"background": "Ảnh nền tuỳ chỉnh (đường dẫn hoặc tệp tin ảnh)",
|
||||
"theme": "Chọn giao diện"
|
||||
"background": "Hình nền tùy chỉnh (liên kết hoặc tập tin hình ảnh)",
|
||||
"theme": "Giao diện",
|
||||
"patch_metadata": "Tự động sửa Metadata",
|
||||
"use_proxy": "Sử dụng proxy nội bộ"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "Tải xuống dữ liệu Grasscutter bản ổn định",
|
||||
"grasscutter_latest_data": "Tải xuống dữ liệu Grasscutter bản mới nhất",
|
||||
"grasscutter_stable_data": "Tải dữ liệu Grasscutter bản ổn định",
|
||||
"grasscutter_latest_data": "Tải dữ liệu Grasscutter bản mới nhất",
|
||||
"grasscutter_stable_data_update": "Cập nhật dữ liệu Grasscutter bản ổn định",
|
||||
"grasscutter_latest_data_update": "Cập nhật dữ liệu Grasscutter bản mới nhất",
|
||||
"grasscutter_stable": "Tải xuống Grasscutter phiên bản ổn định",
|
||||
"grasscutter_latest": "Tải xuống Grasscutter phiển bản mới nhất",
|
||||
"grasscutter_stable_update": "Cập nhật Grasscutter ổn định",
|
||||
"grasscutter_latest_update": "Cập nhật Grasscutter mới nhất",
|
||||
"resources": "Tải xuống tài nguyên cho Grasscutter"
|
||||
"grasscutter_stable": "Tải Grasscutter bản ổn định",
|
||||
"grasscutter_latest": "Tải Grasscutter bản mới nhất",
|
||||
"grasscutter_stable_update": "Cập nhật Grasscutter bản ổn định",
|
||||
"grasscutter_latest_update": "Cập nhật Grasscutter bản mới nhất",
|
||||
"resources": "Tải tài nguyên Grasscutter",
|
||||
"game": "Tải game"
|
||||
},
|
||||
"download_status": {
|
||||
"downloading": "Đang tải",
|
||||
"downloading": "Đang tải xuống",
|
||||
"extracting": "Đang giải nén",
|
||||
"error": "Lỗi",
|
||||
"finished": "Đã xong",
|
||||
"finished": "Hoàn thành",
|
||||
"stopped": "Đã dừng"
|
||||
},
|
||||
"components": {
|
||||
"select_file": "Chọn tệp tin hoặc thư mục...",
|
||||
"select_file": "Chọn tập tin hoặc thư mục...",
|
||||
"select_folder": "Chọn thư mục...",
|
||||
"download": "Tải xuống"
|
||||
"download": "Tải xuống",
|
||||
"install": "Cài đặt"
|
||||
},
|
||||
"news": {
|
||||
"latest_commits": "Cập nhật gần đây",
|
||||
"latest_version": "Phiên bản mới nhất"
|
||||
"latest_commits": "Thay Đổi Gần Đây",
|
||||
"latest_version": "Phiên Bản Mới Nhất"
|
||||
},
|
||||
"help": {
|
||||
"port_help_text": "Đảm bảo đây là cổng của server Dispatch, không phải cổng của server Game. Thường là '443'.",
|
||||
"game_help_text": "Bạn không cần phải sử dụng một bản sao riêng để chơi với Grasscutter. Việc này chỉ xảy ra nếu bạn hạ phiên bản xuống 2.6 hoặc chưa cài đặt trò chơi.",
|
||||
"gc_stable_jar": "Tải xuống phiên bản ổn định của Grasscutter, bảo gồm file jar và các file dữ liệu.",
|
||||
"gc_dev_jar": "Tải xuống phiên bản phát triển mới nhất của Grasscutter, bảo gồm file jar và các file dữ liệu.",
|
||||
"gc_stable_data": "Tải xuống bản ổn định các tệp dữ liệu của Grasscutter, không bao gồm file jar. Phù hợp khi cập nhật.",
|
||||
"gc_dev_data": "Tải xuống bản phát triển mới nhất các tệp dữ liệu của Grasscutter, không bao gồm file jar. Phù hợp khi cập nhật.",
|
||||
"resources": "Chúng được yêu cầu để chạy máy chủ Grasscutter. Nút này sẽ có màu xám nếu bạn có một thư mục tài nguyên có nội dung bên trong"
|
||||
"port_help_text": "Hãy đảm bảo đây là cổng của máy chủ Dispatch, không phải cổng của máy chủ game. Thường sẽ là '443'.",
|
||||
"game_help_text": "Bạn không cần phải sử dụng một bản sao riêng để chơi với Grasscutter. Việc này chỉ xảy ra nếu bạn hạ phiên bản xuống 2.6 hoặc chưa cài game.",
|
||||
"gc_stable_jar": "Tải xuống phiên bản ổn định của Grasscutter, bao gồm tập tin jar và các tệp dữ liệu.",
|
||||
"gc_dev_jar": "Tải xuống phiên bản phát triển mới nhất của Grasscutter, bao gồm tập tin jar và các tệp dữ liệu.",
|
||||
"gc_stable_data": "Tải xuống tệp dữ liệu phiên bản ổn định hiện hành của Grasscutter. Bản này không đi kèm với tập tin jar, hữu dụng khi muốn cập nhật.",
|
||||
"gc_dev_data": "Tải xuống tệp dữ liệu phiên bản mới nhất của Grasscutter. Bản này không đi kèm với tập tin jar, hữu dụng khi muốn cập nhật.",
|
||||
"resources": "Chúng được yêu cầu để chạy máy chủ Grasscutter. Nút này sẽ có màu xám nếu bạn đã có sẵn một thư mục tài nguyên có nội dung bên trong"
|
||||
},
|
||||
"swag": {
|
||||
"akebi": "Tập tin thực thi Akebi",
|
||||
"migoto": "Tập tin thực thi 3dMigoto"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -43,7 +43,7 @@ bool gen_global_metadata_key(uint8_t* src, size_t srcn) {
|
||||
|
||||
uint64_t* key = (uint64_t*)src;
|
||||
|
||||
for (int i = 0; i < srcn / sizeof(uint64_t); i++)
|
||||
for (size_t i = 0; i < srcn / sizeof(uint64_t); i++)
|
||||
key[i] = rand();
|
||||
|
||||
*(uint16_t *) (src + 0xc8) = 0xfc2e; // Magic
|
||||
@@ -53,7 +53,7 @@ bool gen_global_metadata_key(uint8_t* src, size_t srcn) {
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" void decrypt_global_metadata(uint8_t *data, size_t size) {
|
||||
void decrypt_global_metadata_inner(uint8_t *data, size_t size) {
|
||||
uint8_t longkey[0xB00];
|
||||
uint8_t longkeyp[0xB0];
|
||||
uint8_t shortkey[16];
|
||||
@@ -87,7 +87,16 @@ extern "C" void decrypt_global_metadata(uint8_t *data, size_t size) {
|
||||
recrypt_global_metadata_header_string_literals(data, size, literal_dec_key);
|
||||
}
|
||||
|
||||
extern "C" void encrypt_global_metadata(uint8_t* data, size_t size) {
|
||||
extern "C" int decrypt_global_metadata(uint8_t *data, size_t size) {
|
||||
try {
|
||||
decrypt_global_metadata_inner(data, size);
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void encrypt_global_metadata_inner(uint8_t* data, size_t size) {
|
||||
uint8_t literal_dec_key[0x5000];
|
||||
|
||||
gen_global_metadata_key(data + size - 0x4000, 0x4000);
|
||||
@@ -126,3 +135,12 @@ extern "C" void encrypt_global_metadata(uint8_t* data, size_t size) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int encrypt_global_metadata(uint8_t* data, size_t size) {
|
||||
try {
|
||||
encrypt_global_metadata_inner(data, size);
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" void decrypt_global_metadata(uint8_t *data, size_t size);
|
||||
extern "C" void encrypt_global_metadata(uint8_t *data, size_t size);
|
||||
extern "C" int decrypt_global_metadata(uint8_t *data, size_t size);
|
||||
extern "C" int encrypt_global_metadata(uint8_t *data, size_t size);
|
||||
|
||||
#endif //METADATA_H
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
newline_style = "Unix"
|
||||
tab_spaces = 2
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fs;
|
||||
use file_diff::diff;
|
||||
use std::{io::{Read, Write}};
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn rename(path: String, new_name: String) {
|
||||
@@ -18,7 +18,19 @@ pub fn rename(path: String, new_name: String) {
|
||||
|
||||
let path_replaced = &path.replace(&new_path.split('/').last().unwrap(), &new_name);
|
||||
|
||||
fs::rename(path, &path_replaced).unwrap();
|
||||
match fs::rename(&path, &path_replaced) {
|
||||
Ok(_) => {
|
||||
println!("Renamed {} to {}", &path, path_replaced);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn dir_create(path: String) {
|
||||
fs::create_dir_all(path).unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -133,20 +145,12 @@ pub fn write_file(path: String, contents: String) {
|
||||
let path_buf = std::path::PathBuf::from(&path);
|
||||
|
||||
// Create file if it exists, otherwise just open and rewrite
|
||||
let mut file = match fs::File::open(&path_buf) {
|
||||
let mut file = match fs::File::create(&path_buf) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
println!("Failed to open file: {}", e);
|
||||
|
||||
// attempt to create file. otherwise return
|
||||
match fs::File::create(&path_buf) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
println!("Failed to create file: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Write contents to file
|
||||
@@ -154,7 +158,6 @@ pub fn write_file(path: String, contents: String) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!("Failed to write to file: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src-tauri/src/gamebanana.rs
Normal file
88
src-tauri/src/gamebanana.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use crate::file_helpers;
|
||||
use crate::web;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::read_dir;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
static SITE_URL: &str = "https://gamebanana.com";
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_download_links(mod_id: String) -> String {
|
||||
let res = web::query(format!("{}/apiv9/Mod/{}/DownloadPage", SITE_URL, mod_id).as_str()).await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_submissions(mode: String) -> String {
|
||||
let res = web::query(
|
||||
format!(
|
||||
"{}/apiv9/Util/Game/Submissions?_idGameRow=8552&_nPage=1&_nPerpage=50&_sMode={}",
|
||||
SITE_URL, mode
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_mods(path: String) -> HashMap<String, String> {
|
||||
let mut path_buf = PathBuf::from(path);
|
||||
|
||||
// If the path includes a file, remove it
|
||||
if path_buf.file_name().is_some() {
|
||||
path_buf.pop();
|
||||
}
|
||||
|
||||
// Ensure we are in the Mods folder
|
||||
path_buf.push("Mods");
|
||||
|
||||
// Check if dir is empty
|
||||
if file_helpers::dir_is_empty(path_buf.to_str().unwrap()) {
|
||||
return HashMap::new();
|
||||
}
|
||||
|
||||
let mut mod_info_files = vec![];
|
||||
let mut mod_info_strings = HashMap::new();
|
||||
|
||||
for entry in read_dir(path_buf).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
// Check each dir for a modinfo.json file
|
||||
if path.is_dir() {
|
||||
let mut mod_info_path = path.clone();
|
||||
mod_info_path.push("modinfo.json");
|
||||
if mod_info_path.exists() {
|
||||
// Push path AND file contents into the hashmap using path as key
|
||||
mod_info_files.push(mod_info_path.to_str().unwrap().to_string());
|
||||
} else {
|
||||
// No modinfo, but we can still push a JSON obj with the folder name
|
||||
mod_info_strings.insert(
|
||||
path.to_str().unwrap().to_string(),
|
||||
format!(
|
||||
"{{ \"name\": \"{}\" }}",
|
||||
path.file_name().unwrap().to_str().unwrap()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read each modinfo.json file
|
||||
for mod_info_file in mod_info_files {
|
||||
let mut mod_info_string = String::new();
|
||||
|
||||
// It is safe to unwrap here since we *know* that the file exists
|
||||
let mut file = std::fs::File::open(&mod_info_file).unwrap();
|
||||
file.read_to_string(&mut mod_info_string).unwrap();
|
||||
|
||||
// Push into hashmap using path as key
|
||||
mod_info_strings.insert(mod_info_file, mod_info_string);
|
||||
}
|
||||
|
||||
mod_info_strings
|
||||
}
|
||||
@@ -12,17 +12,23 @@ use sysinfo::{System, SystemExt};
|
||||
|
||||
mod downloader;
|
||||
mod file_helpers;
|
||||
mod gamebanana;
|
||||
mod lang;
|
||||
mod metadata_patcher;
|
||||
mod proxy;
|
||||
mod structs;
|
||||
mod system_helpers;
|
||||
mod unzip;
|
||||
mod web;
|
||||
mod metadata_patcher;
|
||||
|
||||
static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
|
||||
|
||||
fn main() {
|
||||
// Always set CWD to the location of the executable.
|
||||
let mut exe_path = std::env::current_exe().unwrap();
|
||||
exe_path.pop();
|
||||
std::env::set_current_dir(&exe_path).unwrap();
|
||||
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
enable_process_watcher,
|
||||
@@ -34,6 +40,7 @@ fn main() {
|
||||
get_theme_list,
|
||||
system_helpers::run_command,
|
||||
system_helpers::run_program,
|
||||
system_helpers::run_program_relative,
|
||||
system_helpers::run_jar,
|
||||
system_helpers::open_in_browser,
|
||||
system_helpers::install_location,
|
||||
@@ -42,6 +49,7 @@ fn main() {
|
||||
proxy::generate_ca_files,
|
||||
unzip::unzip,
|
||||
file_helpers::rename,
|
||||
file_helpers::dir_create,
|
||||
file_helpers::dir_exists,
|
||||
file_helpers::dir_is_empty,
|
||||
file_helpers::dir_delete,
|
||||
@@ -57,6 +65,9 @@ fn main() {
|
||||
lang::get_languages,
|
||||
web::valid_url,
|
||||
web::web_get,
|
||||
gamebanana::get_download_links,
|
||||
gamebanana::list_submissions,
|
||||
gamebanana::list_mods,
|
||||
metadata_patcher::patch_metadata
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
@@ -72,7 +83,7 @@ fn is_game_running() -> bool {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn enable_process_watcher(window: tauri::Window,process: String) {
|
||||
fn enable_process_watcher(window: tauri::Window, process: String) {
|
||||
*WATCH_GAME_PROCESS.lock().unwrap() = process;
|
||||
|
||||
window.listen("disable_process_watcher", |_e| {
|
||||
@@ -82,6 +93,9 @@ fn enable_process_watcher(window: tauri::Window,process: String) {
|
||||
println!("Starting process watcher...");
|
||||
|
||||
thread::spawn(move || {
|
||||
// Initial sleep for 8 seconds, since running 20 different injectors or whatever can take a while
|
||||
std::thread::sleep(std::time::Duration::from_secs(8));
|
||||
|
||||
let mut system = System::new_all();
|
||||
|
||||
loop {
|
||||
|
||||
@@ -4,23 +4,10 @@ use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
|
||||
extern {
|
||||
fn decrypt_global_metadata(data : *mut u8, size : u64);
|
||||
fn encrypt_global_metadata(data : *mut u8, size : u64);
|
||||
}
|
||||
|
||||
fn dll_decrypt_global_metadata(data : *mut u8, size : u64) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
unsafe {
|
||||
decrypt_global_metadata(data, size);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn dll_encrypt_global_metadata(data : *mut u8, size : u64) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
unsafe {
|
||||
encrypt_global_metadata(data, size);
|
||||
Ok(true)
|
||||
}
|
||||
// For these two functions, a non-zero return value indicates failure.
|
||||
extern "C" {
|
||||
fn decrypt_global_metadata(data: *mut u8, size: usize) -> i32;
|
||||
fn encrypt_global_metadata(data: *mut u8, size: usize) -> i32;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -46,7 +33,11 @@ pub fn patch_metadata(metadata_folder: &str) -> bool {
|
||||
}
|
||||
|
||||
//write encrypted to file
|
||||
let mut file = match OpenOptions::new().create(true).write(true).open(&(metadata_folder.to_owned() + "\\global-metadata-patched.dat")) {
|
||||
let mut file = match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(&(metadata_folder.to_owned() + "\\global-metadata-patched.dat"))
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
println!("Failed to open global-metadata: {}", e);
|
||||
@@ -79,18 +70,14 @@ fn decrypt_metadata(file_path: &str) -> Vec<u8> {
|
||||
}
|
||||
|
||||
// Decrypt metadata file
|
||||
match dll_decrypt_global_metadata(data.as_mut_ptr(), data.len().try_into().unwrap()) {
|
||||
Ok(_) => {
|
||||
let success = unsafe { decrypt_global_metadata(data.as_mut_ptr(), data.len()) } == 0;
|
||||
if success {
|
||||
println!("Successfully decrypted global-metadata");
|
||||
data
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to decrypt global-metadata: {}", e);
|
||||
} else {
|
||||
println!("Failed to decrypt global-metadata");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn replace_keys(data: &[u8]) -> Vec<u8> {
|
||||
@@ -124,7 +111,12 @@ fn replace_keys(data: &[u8]) -> Vec<u8> {
|
||||
fn replace_rsa_key(old_data: &str, to_replace: &str, file_name: &str) -> String {
|
||||
// Read dispatch key file
|
||||
unsafe {
|
||||
let mut new_key_file = match File::open(&("keys/".to_owned() + file_name)) {
|
||||
// Get key folder from exe path
|
||||
let mut exe_path = std::env::current_exe().unwrap();
|
||||
exe_path.pop();
|
||||
|
||||
let key_folder = exe_path.to_str().unwrap().to_string();
|
||||
let mut new_key_file = match File::open(format!("{}/keys/{}", key_folder, file_name)) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
println!("Failed to open keys/{}: {}", file_name, e);
|
||||
@@ -142,21 +134,16 @@ fn replace_rsa_key(old_data: &str, to_replace: &str, file_name: &str) -> String
|
||||
|
||||
fn encrypt_metadata(old_data: &[u8]) -> Vec<u8> {
|
||||
let mut data = old_data.to_vec();
|
||||
match dll_encrypt_global_metadata(data.as_mut_ptr(), data.len().try_into().unwrap()) {
|
||||
Ok(_) => {
|
||||
let success = unsafe { encrypt_global_metadata(data.as_mut_ptr(), data.len()) } == 0;
|
||||
if success {
|
||||
println!("Successfully encrypted global-metadata");
|
||||
data
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to encrypt global-metadata: {}", e);
|
||||
} else {
|
||||
println!("Failed to encrypt global-metadata");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn do_vecs_match<T: PartialEq>(a: &Vec<T>, b: &Vec<T>) -> bool {
|
||||
let matching = a.iter().zip(b.iter()).filter(|&(a, b)| a == b).count();
|
||||
matching == a.len() && matching == b.len()
|
||||
a == b
|
||||
}
|
||||
@@ -242,6 +242,7 @@ pub fn install_ca_files(cert_path: &Path) {
|
||||
crate::system_helpers::run_command(
|
||||
"certutil",
|
||||
vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()],
|
||||
None,
|
||||
);
|
||||
println!("Installed certificate.");
|
||||
}
|
||||
|
||||
@@ -1,17 +1,54 @@
|
||||
use duct::cmd;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn run_program(path: String) {
|
||||
// Open in new thread to prevent blocking.
|
||||
std::thread::spawn(move || {
|
||||
pub fn run_program(path: String, args: Option<String>) {
|
||||
// Without unwrap_or, this can crash when UAC prompt is denied
|
||||
open::that(&path).unwrap_or(());
|
||||
});
|
||||
open::that(format!("{} {}", &path, &args.unwrap_or("".into()))).unwrap_or(());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn run_command(program: &str, args: Vec<&str>) {
|
||||
cmd(program, args).run().expect("Failed to run command");
|
||||
pub fn run_program_relative(path: String, args: Option<String>) {
|
||||
// Save the current working directory
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
|
||||
// Set the new working directory to the path before the executable
|
||||
let mut path_buf = std::path::PathBuf::from(&path);
|
||||
path_buf.pop();
|
||||
|
||||
// Set new working directory
|
||||
std::env::set_current_dir(&path_buf).unwrap();
|
||||
|
||||
// Without unwrap_or, this can crash when UAC prompt is denied
|
||||
open::that(format!("{} {}", &path, args.unwrap_or("".into()))).unwrap_or(());
|
||||
|
||||
// Restore the original working directory
|
||||
std::env::set_current_dir(&cwd).unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn run_command(program: &str, args: Vec<&str>, relative: Option<bool>) {
|
||||
let prog = program.to_string();
|
||||
let args = args.iter().map(|s| s.to_string()).collect::<Vec<String>>();
|
||||
|
||||
// Commands should not block (this is for the reshade injector mostly)
|
||||
std::thread::spawn(move || {
|
||||
// Save the current working directory
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
|
||||
if relative.unwrap_or(false) {
|
||||
// Set the new working directory to the path before the executable
|
||||
let mut path_buf = std::path::PathBuf::from(&prog);
|
||||
path_buf.pop();
|
||||
|
||||
// Set new working directory
|
||||
std::env::set_current_dir(&path_buf).unwrap();
|
||||
}
|
||||
|
||||
cmd(prog, args).run().unwrap();
|
||||
|
||||
// Restore the original working directory
|
||||
std::env::set_current_dir(&cwd).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use std::fs::File;
|
||||
use std::fs::{read_dir, File};
|
||||
use std::path;
|
||||
use std::thread;
|
||||
use unrar::archive::Archive;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
|
||||
pub fn unzip(
|
||||
window: tauri::Window,
|
||||
zipfile: String,
|
||||
destpath: String,
|
||||
top_level: Option<bool>,
|
||||
folder_if_loose: Option<bool>,
|
||||
) {
|
||||
// Read file TODO: replace test file
|
||||
let f = match File::open(&zipfile) {
|
||||
Ok(f) => f,
|
||||
@@ -15,40 +22,80 @@ pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
|
||||
|
||||
let write_path = path::PathBuf::from(&destpath);
|
||||
|
||||
// Get a list of all current directories
|
||||
let mut dirs = vec![];
|
||||
for entry in read_dir(&write_path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let entry_path = entry.path();
|
||||
if entry_path.is_dir() {
|
||||
dirs.push(entry_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Run extraction in seperate thread
|
||||
thread::spawn(move || {
|
||||
let full_path = write_path;
|
||||
let mut full_path = write_path.clone();
|
||||
|
||||
window.emit("extract_start", &zipfile).unwrap();
|
||||
|
||||
match zip_extract::extract(&f, &full_path, true) {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Extracted zip file to: {}",
|
||||
full_path.to_str().unwrap_or("Error")
|
||||
);
|
||||
}
|
||||
if folder_if_loose.unwrap_or(false) {
|
||||
// Create a new folder with the same name as the zip file
|
||||
let mut file_name = path::Path::new(&zipfile)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
// remove ".zip" from the end of the file name
|
||||
file_name = &file_name[..file_name.len() - 4];
|
||||
|
||||
let new_path = full_path.join(file_name);
|
||||
match std::fs::create_dir_all(&new_path) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("Failed to extract zip file: {}", e);
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
res_hash.insert("error".to_string(), e.to_string());
|
||||
|
||||
res_hash.insert("path".to_string(), zipfile.to_string());
|
||||
|
||||
window.emit("download_error", &res_hash).unwrap();
|
||||
println!("Failed to create directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
full_path = new_path.clone();
|
||||
}
|
||||
|
||||
println!("Is rar file? {}", zipfile.ends_with(".rar"));
|
||||
|
||||
let name;
|
||||
|
||||
// If file ends in zip, OR is unknown, extract as zip, otherwise extract as rar
|
||||
if zipfile.ends_with(".rar") {
|
||||
extract_rar(
|
||||
&window,
|
||||
&zipfile,
|
||||
&f,
|
||||
&full_path,
|
||||
top_level.unwrap_or(false),
|
||||
);
|
||||
|
||||
let archive = Archive::new(zipfile.clone());
|
||||
name = archive.list().unwrap().next().unwrap().unwrap().filename;
|
||||
} else {
|
||||
extract_zip(
|
||||
&window,
|
||||
&zipfile,
|
||||
&f,
|
||||
&full_path,
|
||||
top_level.unwrap_or(false),
|
||||
);
|
||||
|
||||
// Get the name of the inenr file in the zip file
|
||||
let mut zip = zip::ZipArchive::new(&f).unwrap();
|
||||
let file = zip.by_index(0).unwrap();
|
||||
let name = file.name();
|
||||
name = file.name().to_string().clone();
|
||||
}
|
||||
|
||||
// If the contents is a jar file, emit that we have extracted a new jar file
|
||||
if name.ends_with(".jar") {
|
||||
window
|
||||
.emit("jar_extracted", destpath.to_string() + name)
|
||||
.emit("jar_extracted", destpath.to_string() + name.as_str())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -62,6 +109,80 @@ pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
|
||||
}
|
||||
};
|
||||
|
||||
window.emit("extract_end", &zipfile).unwrap();
|
||||
// Get any new directory that could have been created
|
||||
let mut new_dir: String = String::new();
|
||||
for entry in read_dir(&write_path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let entry_path = entry.path();
|
||||
if entry_path.is_dir() {
|
||||
if !dirs.contains(&entry_path) {
|
||||
new_dir = entry_path.to_str().unwrap().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
res_hash.insert("file", zipfile.to_string());
|
||||
res_hash.insert("new_folder", new_dir.to_string());
|
||||
|
||||
window.emit("extract_end", &res_hash).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_rar(
|
||||
window: &tauri::Window,
|
||||
rarfile: &String,
|
||||
_f: &File,
|
||||
full_path: &path::PathBuf,
|
||||
_top_level: bool,
|
||||
) {
|
||||
let archive = Archive::new(rarfile.clone());
|
||||
|
||||
let mut open_archive = archive
|
||||
.extract_to(full_path.to_str().unwrap().to_string())
|
||||
.unwrap();
|
||||
|
||||
match open_archive.process() {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Extracted rar file to: {}",
|
||||
full_path.to_str().unwrap_or("Error")
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to extract rar file: {}", e);
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
res_hash.insert("error".to_string(), e.to_string());
|
||||
res_hash.insert("path".to_string(), rarfile.to_string());
|
||||
|
||||
window.emit("download_error", &res_hash).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_zip(
|
||||
window: &tauri::Window,
|
||||
zipfile: &String,
|
||||
f: &File,
|
||||
full_path: &path::PathBuf,
|
||||
top_level: bool,
|
||||
) {
|
||||
match zip_extract::extract(f, full_path, top_level) {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Extracted zip file to: {}",
|
||||
full_path.to_str().unwrap_or("Error")
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to extract zip file: {}", e);
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
res_hash.insert("error".to_string(), e.to_string());
|
||||
res_hash.insert("path".to_string(), zipfile.to_string());
|
||||
|
||||
window.emit("download_error", &res_hash).unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use reqwest::header::USER_AGENT;
|
||||
use reqwest::header::{CONTENT_TYPE, USER_AGENT};
|
||||
|
||||
pub(crate) async fn query(site: &str) -> String {
|
||||
let client = reqwest::Client::new();
|
||||
@@ -6,6 +6,7 @@ pub(crate) async fn query(site: &str) -> String {
|
||||
let response = client
|
||||
.get(site)
|
||||
.header(USER_AGENT, "cultivation")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -7,25 +7,17 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Cultivation",
|
||||
"version": "1.0.3"
|
||||
"version": "1.0.6"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"fs": {
|
||||
"scope": [
|
||||
"$DATA",
|
||||
"$DATA/cultivation",
|
||||
"$DATA/cultivation/*"
|
||||
]
|
||||
"scope": ["$DATA", "$DATA/cultivation", "$DATA/cultivation/*"]
|
||||
},
|
||||
"protocol": {
|
||||
"all": true,
|
||||
"asset": true,
|
||||
"assetScope": [
|
||||
"$DATA",
|
||||
"$DATA/cultivation",
|
||||
"$DATA/cultivation/*"
|
||||
]
|
||||
"assetScope": ["$DATA", "$DATA/cultivation", "$DATA/cultivation/*"]
|
||||
},
|
||||
"all": true
|
||||
},
|
||||
@@ -37,13 +29,7 @@
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"identifier": "io.grasscutter",
|
||||
"shortDescription": "A game launcher.",
|
||||
"longDescription": "A launcher for a certain anime game that proxies all related game traffic to external servers.",
|
||||
@@ -54,11 +40,7 @@
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [
|
||||
"lang/*.json",
|
||||
"keys/*",
|
||||
"./mhycrypto.dll"
|
||||
],
|
||||
"resources": ["lang/*.json", "keys/*"],
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"allowDowngrades": false,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'MiHoYo_SDK_Web', 'Helvetica Neue', BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
sans-serif;
|
||||
font-family: 'MiHoYo_SDK_Web', 'Helvetica Neue', BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
@@ -7,23 +7,15 @@ import Debug from './ui/Debug'
|
||||
|
||||
import { getConfigOption } from './utils/configuration'
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
)
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
let isDebug = false;
|
||||
let isDebug = false
|
||||
|
||||
(async() => {
|
||||
;async () => {
|
||||
isDebug = await getConfigOption('debug_enabled')
|
||||
})
|
||||
}
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
{
|
||||
isDebug ? <Debug /> : <App />
|
||||
}
|
||||
</React.StrictMode>
|
||||
)
|
||||
root.render(<React.StrictMode>{isDebug ? <Debug /> : <App />}</React.StrictMode>)
|
||||
|
||||
import reportWebVitals from './utils/reportWebVitals'
|
||||
isDebug && reportWebVitals(console.log)
|
||||
10
src/resources/icons/back.svg
Normal file
10
src/resources/icons/back.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 45 0 c 24.813 0 45 20.187 45 45 c 0 24.813 -20.187 45 -45 45 C 20.186 90 0 69.813 0 45 C 0 20.187 20.186 0 45 0 z M 51.263 73.4 l 8.6 -8.6 L 40.064 45 l 19.799 -19.799 l -8.6 -8.6 L 22.864 45 L 51.263 73.4 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1002 B |
11
src/resources/icons/eye.svg
Normal file
11
src/resources/icons/eye.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 89.307 43.082 C 74.775 25.601 59.868 16.737 45 16.737 c -14.869 0 -29.775 8.864 -44.307 26.345 c -0.924 1.112 -0.924 2.724 0 3.836 C 15.225 64.399 30.131 73.264 45 73.264 c 14.868 0 29.775 -8.864 44.307 -26.346 C 90.231 45.806 90.231 44.194 89.307 43.082 z M 45 62 c -9.374 0 -17 -7.626 -17 -17 s 7.626 -17 17 -17 s 17 7.626 17 17 S 54.374 62 45 62 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<circle cx="45" cy="45" r="9" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
11
src/resources/icons/like.svg
Normal file
11
src/resources/icons/like.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 0 87.201 h 18.478 c 1.44 0 2.607 -1.167 2.607 -2.607 V 35.343 c 0 -1.44 -1.167 -2.607 -2.607 -2.607 H 0 L 0 87.201 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 83.186 46.32 c 3.763 0 6.814 -3.051 6.814 -6.814 c 0 -3.763 -3.051 -6.814 -6.814 -6.814 H 61.591 c 3.758 -6.768 3.872 -27.328 -5.046 -29.797 c -1.689 -0.468 -3.365 0.823 -3.554 2.565 c -1.568 14.428 -10.395 32.362 -19.37 32.881 h -6.991 v 43.003 h 3.627 c 1.952 0 3.817 0.666 5.444 1.743 c 4.063 2.691 10.906 4.265 17.465 4.101 h 3.172 v 0.012 h 21.788 c 3.763 0 6.814 -3.051 6.814 -6.814 c 0 -3.763 -3.051 -6.814 -6.814 -6.814 h 3.037 c 3.763 0 6.814 -3.051 6.814 -6.814 c 0 -3.763 -3.051 -6.814 -6.814 -6.814 h 2.025 c 3.763 0 6.814 -3.051 6.814 -6.814 S 86.949 46.32 83.186 46.32 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
11
src/resources/icons/plus.svg
Normal file
11
src/resources/icons/plus.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 58.921 90 H 31.079 c -1.155 0 -2.092 -0.936 -2.092 -2.092 V 2.092 C 28.988 0.936 29.924 0 31.079 0 h 27.841 c 1.155 0 2.092 0.936 2.092 2.092 v 85.817 C 61.012 89.064 60.076 90 58.921 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 90 31.079 v 27.841 c 0 1.155 -0.936 2.092 -2.092 2.092 H 2.092 C 0.936 61.012 0 60.076 0 58.921 V 31.079 c 0 -1.155 0.936 -2.092 2.092 -2.092 h 85.817 C 89.064 28.988 90 29.924 90 31.079 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
10
src/resources/icons/wrench.svg
Normal file
10
src/resources/icons/wrench.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 87.12 73.212 L 48.774 34.866 c 3.723 -9.153 1.873 -20.042 -5.553 -27.468 c -7.008 -7.008 -17.094 -9.025 -25.9 -6.104 L 28.96 12.934 c 4.387 4.387 4.833 11.594 0.579 16.112 c -4.402 4.674 -11.761 4.757 -16.268 0.25 L 1.295 17.32 c -2.922 8.807 -0.904 18.892 6.104 25.9 c 7.426 7.426 18.315 9.276 27.468 5.553 L 73.212 87.12 c 3.84 3.84 10.067 3.84 13.908 0 l 0 0 C 90.96 83.279 90.96 77.052 87.12 73.212 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -22,7 +22,8 @@ select:focus {
|
||||
border-bottom-color: #ffd326;
|
||||
}
|
||||
|
||||
#root, .App {
|
||||
#root,
|
||||
.App {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -34,12 +35,29 @@ select:focus {
|
||||
.TopButton {
|
||||
height: 60%;
|
||||
margin: 0px 10px;
|
||||
|
||||
color: #c5c5c5;
|
||||
transition: color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.TopButton span {
|
||||
display: inline-block;
|
||||
line-height: normal;
|
||||
border-bottom: 1px solid #c5c5c5;
|
||||
}
|
||||
|
||||
.TopButton img {
|
||||
filter: invert(95%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(88%) contrast(81%);
|
||||
|
||||
transition: filter 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.TopButton:hover {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.TopButton:hover img {
|
||||
filter: invert(100%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(100%) contrast(100%);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -96,13 +114,13 @@ select:focus {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media(max-height: 580px) {
|
||||
@media (max-height: 580px) {
|
||||
.BottomSection {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-height: 500px) {
|
||||
@media (max-height: 500px) {
|
||||
.BottomSection {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
232
src/ui/App.tsx
232
src/ui/App.tsx
@@ -1,95 +1,33 @@
|
||||
import React from 'react'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import './App.css'
|
||||
|
||||
import DownloadHandler from '../utils/download'
|
||||
|
||||
// Major Components
|
||||
import TopBar from './components/TopBar'
|
||||
import ServerLaunchSection from './components/ServerLaunchSection'
|
||||
import MainProgressBar from './components/common/MainProgressBar'
|
||||
import Options from './components/menu/Options'
|
||||
import MiniDialog from './components/MiniDialog'
|
||||
import DownloadList from './components/common/DownloadList'
|
||||
import Downloads from './components/menu/Downloads'
|
||||
import NewsSection from './components/news/NewsSection'
|
||||
import Game from './components/menu/Game'
|
||||
|
||||
import RightBar from './components/RightBar'
|
||||
import { getConfigOption, setConfigOption } from '../utils/configuration'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { getConfigOption } from '../utils/configuration'
|
||||
import { getTheme, loadTheme } from '../utils/themes'
|
||||
import { unpatchGame } from '../utils/metadata'
|
||||
|
||||
interface IProps {
|
||||
[key: string]: never;
|
||||
}
|
||||
import { convertFileSrc, invoke } from '@tauri-apps/api/tauri'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { Main } from './Main'
|
||||
import { Mods } from './Mods'
|
||||
|
||||
interface IState {
|
||||
isDownloading: boolean;
|
||||
optionsOpen: boolean;
|
||||
miniDownloadsOpen: boolean;
|
||||
downloadsOpen: boolean;
|
||||
gameDownloadsOpen: boolean;
|
||||
bgFile: string;
|
||||
page: string
|
||||
bgFile: string
|
||||
}
|
||||
|
||||
const downloadHandler = new DownloadHandler()
|
||||
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
|
||||
|
||||
const downloadHandler = new DownloadHandler()
|
||||
|
||||
class App extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
class App extends React.Component<Readonly<unknown>, IState> {
|
||||
constructor(props: Readonly<unknown>) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isDownloading: false,
|
||||
optionsOpen: false,
|
||||
miniDownloadsOpen: false,
|
||||
downloadsOpen: false,
|
||||
gameDownloadsOpen: false,
|
||||
page: 'main',
|
||||
bgFile: DEFAULT_BG,
|
||||
}
|
||||
|
||||
listen('lang_error', (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
|
||||
listen('jar_extracted', ({ payload }: { payload: string}) => {
|
||||
setConfigOption('grasscutter_path', payload)
|
||||
})
|
||||
|
||||
// Emitted for metadata replacing-purposes
|
||||
listen('game_closed', async () => {
|
||||
const unpatched = await unpatchGame()
|
||||
|
||||
console.log(`unpatched game? ${unpatched}`)
|
||||
|
||||
if (!unpatched) {
|
||||
alert(`Could not unpatch game! (You should be able to find your metadata backup in ${await dataDir()}\\cultivation\\)`)
|
||||
}
|
||||
})
|
||||
|
||||
let min = false
|
||||
|
||||
// periodically check if we need to min/max based on whether the game is open
|
||||
setInterval(async () => {
|
||||
const gameOpen = await invoke('is_game_running')
|
||||
|
||||
if (gameOpen && !min) {
|
||||
appWindow.minimize()
|
||||
min = true
|
||||
} else if (!gameOpen && min) {
|
||||
appWindow.unminimize()
|
||||
min = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const cert_generated = await getConfigOption('cert_generated')
|
||||
const game_exe = await getConfigOption('game_install_path')
|
||||
const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || ''
|
||||
const root_path = game_path?.substring(0, game_path.replace(/\\/g, '/').lastIndexOf('/')) || ''
|
||||
@@ -104,135 +42,79 @@ class App extends React.Component<IProps, IState> {
|
||||
// Get custom bg AFTER theme is loaded !! important !!
|
||||
const custom_bg = await getConfigOption('customBackground')
|
||||
|
||||
if(!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) {
|
||||
if(game_path) {
|
||||
if (!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) {
|
||||
if (game_path) {
|
||||
// Get the bg by invoking, then set the background to that bg.
|
||||
const bgLoc: string = await invoke('get_bg_file', {
|
||||
bgPath: root_path,
|
||||
appdata: await dataDir()
|
||||
appdata: await dataDir(),
|
||||
})
|
||||
|
||||
bgLoc && this.setState({
|
||||
bgFile: bgLoc
|
||||
}, this.forceUpdate)
|
||||
bgLoc &&
|
||||
this.setState(
|
||||
{
|
||||
bgFile: bgLoc,
|
||||
},
|
||||
this.forceUpdate
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const isUrl = /^http(s)?:\/\//gm.test(custom_bg)
|
||||
|
||||
if (!isUrl) {
|
||||
const isValid = await invoke('dir_exists', {
|
||||
path: custom_bg
|
||||
path: custom_bg,
|
||||
})
|
||||
|
||||
this.setState({
|
||||
bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG
|
||||
}, this.forceUpdate)
|
||||
this.setState(
|
||||
{
|
||||
bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG,
|
||||
},
|
||||
this.forceUpdate
|
||||
)
|
||||
} else {
|
||||
// Check if URL returns a valid image.
|
||||
const isValid = await invoke('valid_url', {
|
||||
url: custom_bg
|
||||
url: custom_bg,
|
||||
})
|
||||
|
||||
this.setState(
|
||||
{
|
||||
bgFile: isValid ? custom_bg : DEFAULT_BG,
|
||||
},
|
||||
this.forceUpdate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('changePage', (e) => {
|
||||
this.setState({
|
||||
bgFile: isValid ? custom_bg : DEFAULT_BG
|
||||
}, this.forceUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
if (!cert_generated) {
|
||||
// Generate the certificate
|
||||
await invoke('generate_ca_files', {
|
||||
path: await dataDir() + 'cultivation'
|
||||
// @ts-expect-error - TS doesn't like our custom event
|
||||
page: e.detail,
|
||||
})
|
||||
|
||||
await setConfigOption('cert_generated', true)
|
||||
}
|
||||
|
||||
// Period check to only show progress bar when downloading files
|
||||
setInterval(() => {
|
||||
this.setState({
|
||||
isDownloading: downloadHandler.getDownloads().filter(d => d.status !== 'finished')?.length > 0
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="App" style={
|
||||
this.state.bgFile ? {
|
||||
<div
|
||||
className="App"
|
||||
style={
|
||||
this.state.bgFile
|
||||
? {
|
||||
background: `url("${this.state.bgFile}") fixed`,
|
||||
} : {}
|
||||
}>
|
||||
<TopBar
|
||||
optFunc={() => {
|
||||
this.setState({ optionsOpen: !this.state.optionsOpen })
|
||||
}}
|
||||
downFunc={() => this.setState({ downloadsOpen: !this.state.downloadsOpen })}
|
||||
gameFunc={() => this.setState({ gameDownloadsOpen: !this.state.gameDownloadsOpen })}
|
||||
/>
|
||||
|
||||
<RightBar />
|
||||
|
||||
<NewsSection />
|
||||
|
||||
{
|
||||
// Mini downloads section
|
||||
this.state.miniDownloadsOpen ? (
|
||||
<div className="MiniDownloads" id="miniDownloadContainer">
|
||||
<MiniDialog
|
||||
title="Downloads"
|
||||
closeFn={() => {
|
||||
this.setState({ miniDownloadsOpen: false })
|
||||
}}
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<DownloadList downloadManager={downloadHandler} />
|
||||
</MiniDialog>
|
||||
<div className="arrow-down"></div>
|
||||
</div>
|
||||
) : null
|
||||
{(() => {
|
||||
switch (this.state.page) {
|
||||
case 'modding':
|
||||
return <Mods downloadHandler={downloadHandler} />
|
||||
default:
|
||||
return <Main downloadHandler={downloadHandler} />
|
||||
}
|
||||
|
||||
{
|
||||
// Download menu
|
||||
this.state.downloadsOpen ? (
|
||||
<Downloads
|
||||
downloadManager={downloadHandler}
|
||||
closeFn={() => this.setState({ downloadsOpen: false })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Options menu
|
||||
this.state.optionsOpen ? (
|
||||
<Options
|
||||
downloadManager={downloadHandler}
|
||||
closeFn={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Game downloads menu
|
||||
this.state.gameDownloadsOpen ? (
|
||||
<Game
|
||||
downloadManager={downloadHandler}
|
||||
closeFn={() => this.setState({ gameDownloadsOpen: false })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
<div className="BottomSection" id="bottomSectionContainer">
|
||||
<ServerLaunchSection />
|
||||
|
||||
<div id="DownloadProgress"
|
||||
onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })}
|
||||
>
|
||||
{ this.state.isDownloading ?
|
||||
<MainProgressBar downloadManager={downloadHandler} />
|
||||
: null }
|
||||
</div>
|
||||
</div>
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import './App.css'
|
||||
|
||||
import TopBar from './components/TopBar'
|
||||
|
||||
import {invoke} from '@tauri-apps/api/tauri'
|
||||
import {dataDir} from '@tauri-apps/api/path'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import TextInput from './components/common/TextInput'
|
||||
|
||||
let proxyAddress = ''
|
||||
@@ -15,7 +15,7 @@ async function setProxyAddress(address: string) {
|
||||
}
|
||||
|
||||
async function startProxy() {
|
||||
await invoke('connect', { port: 2222, certificatePath: await dataDir() + '\\cultivation\\ca' })
|
||||
await invoke('connect', { port: 2222, certificatePath: (await dataDir()) + '\\cultivation\\ca' })
|
||||
await invoke('open_in_browser', { url: 'https://hoyoverse.com' })
|
||||
}
|
||||
|
||||
@@ -24,27 +24,23 @@ async function stopProxy() {
|
||||
}
|
||||
|
||||
async function generateCertificates() {
|
||||
await invoke('generate_ca_files', { path: await dataDir() + '\\cultivation' })
|
||||
await invoke('generate_ca_files', { path: (await dataDir()) + '\\cultivation' })
|
||||
}
|
||||
|
||||
async function generateInfo() {
|
||||
console.log({
|
||||
certificatePath: await dataDir() + '\\cultivation\\ca',
|
||||
certificatePath: (await dataDir()) + '\\cultivation\\ca',
|
||||
isAdmin: await invoke('is_elevated'),
|
||||
connectingTo: proxyAddress
|
||||
connectingTo: proxyAddress,
|
||||
})
|
||||
alert('check your dev console and send that in #cultivation')
|
||||
}
|
||||
|
||||
function none() {
|
||||
alert('none')
|
||||
}
|
||||
|
||||
class Debug extends React.Component{
|
||||
class Debug extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="App">
|
||||
<TopBar optFunc={none} downFunc={none} gameFunc={none} />
|
||||
<TopBar />
|
||||
<TextInput readOnly={false} initalValue={'change to set proxy address'} onChange={setProxyAddress} />
|
||||
<button onClick={startProxy}>start proxy</button>
|
||||
<button onClick={stopProxy}>stop proxy</button>
|
||||
|
||||
241
src/ui/Main.tsx
Normal file
241
src/ui/Main.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import React from 'react'
|
||||
|
||||
// Major Components
|
||||
import TopBar from './components/TopBar'
|
||||
import ServerLaunchSection from './components/ServerLaunchSection'
|
||||
import MainProgressBar from './components/common/MainProgressBar'
|
||||
import Options from './components/menu/Options'
|
||||
import MiniDialog from './components/MiniDialog'
|
||||
import DownloadList from './components/common/DownloadList'
|
||||
import Downloads from './components/menu/Downloads'
|
||||
import NewsSection from './components/news/NewsSection'
|
||||
import Game from './components/menu/Game'
|
||||
import RightBar from './components/RightBar'
|
||||
|
||||
import { getConfigOption, setConfigOption } from '../utils/configuration'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { unpatchGame } from '../utils/metadata'
|
||||
import DownloadHandler from '../utils/download'
|
||||
|
||||
// Graphics
|
||||
import cogBtn from '../resources/icons/cog.svg'
|
||||
import downBtn from '../resources/icons/download.svg'
|
||||
import wrenchBtn from '../resources/icons/wrench.svg'
|
||||
import { ExtrasMenu } from './components/menu/ExtrasMenu'
|
||||
|
||||
interface IProps {
|
||||
downloadHandler: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isDownloading: boolean
|
||||
optionsOpen: boolean
|
||||
miniDownloadsOpen: boolean
|
||||
downloadsOpen: boolean
|
||||
gameDownloadsOpen: boolean
|
||||
extrasOpen: boolean
|
||||
migotoSet: boolean
|
||||
playGame: (exe?: string, proc_name?: string) => void
|
||||
}
|
||||
|
||||
export class Main extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isDownloading: false,
|
||||
optionsOpen: false,
|
||||
miniDownloadsOpen: false,
|
||||
downloadsOpen: false,
|
||||
gameDownloadsOpen: false,
|
||||
extrasOpen: false,
|
||||
migotoSet: false,
|
||||
playGame: () => {
|
||||
alert('Error launching game')
|
||||
},
|
||||
}
|
||||
|
||||
listen('lang_error', (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
|
||||
listen('jar_extracted', ({ payload }: { payload: string }) => {
|
||||
setConfigOption('grasscutter_path', payload)
|
||||
})
|
||||
|
||||
// Emitted for metadata replacing-purposes
|
||||
listen('game_closed', async () => {
|
||||
const wasPatched = await getConfigOption('patch_metadata')
|
||||
|
||||
if (wasPatched) {
|
||||
const unpatched = await unpatchGame()
|
||||
|
||||
if (!unpatched) {
|
||||
alert(
|
||||
`Could not unpatch game! (You should be able to find your metadata backup in ${await dataDir()}\\cultivation\\)`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let min = false
|
||||
|
||||
// periodically check if we need to min/max based on whether the game is open
|
||||
setInterval(async () => {
|
||||
const gameOpen = await invoke('is_game_running')
|
||||
|
||||
if (gameOpen && !min) {
|
||||
appWindow.minimize()
|
||||
min = true
|
||||
} else if (!gameOpen && min) {
|
||||
appWindow.unminimize()
|
||||
min = false
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
this.openExtrasMenu = this.openExtrasMenu.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const cert_generated = await getConfigOption('cert_generated')
|
||||
|
||||
this.setState({
|
||||
migotoSet: !!(await getConfigOption('migoto_path')),
|
||||
})
|
||||
|
||||
if (!cert_generated) {
|
||||
// Generate the certificate
|
||||
await invoke('generate_ca_files', {
|
||||
path: (await dataDir()) + 'cultivation',
|
||||
})
|
||||
|
||||
await setConfigOption('cert_generated', true)
|
||||
}
|
||||
|
||||
// Period check to only show progress bar when downloading files
|
||||
setInterval(() => {
|
||||
this.setState({
|
||||
isDownloading: this.props.downloadHandler.getDownloads().filter((d) => d.status !== 'finished')?.length > 0,
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async openExtrasMenu(playGame: () => void) {
|
||||
this.setState({
|
||||
extrasOpen: true,
|
||||
playGame,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<TopBar>
|
||||
<div
|
||||
id="settingsBtn"
|
||||
onClick={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
|
||||
className="TopButton"
|
||||
>
|
||||
<img src={cogBtn} alt="settings" />
|
||||
</div>
|
||||
<div
|
||||
id="downloadsBtn"
|
||||
className="TopButton"
|
||||
onClick={() => this.setState({ downloadsOpen: !this.state.downloadsOpen })}
|
||||
>
|
||||
<img src={downBtn} alt="downloads" />
|
||||
</div>
|
||||
{this.state.migotoSet && (
|
||||
<div
|
||||
id="modsBtn"
|
||||
onClick={() => {
|
||||
// Create and dispatch a custom "openMods" event
|
||||
const event = new CustomEvent('changePage', { detail: 'modding' })
|
||||
window.dispatchEvent(event)
|
||||
}}
|
||||
className="TopButton"
|
||||
>
|
||||
<img src={wrenchBtn} alt="mods" />
|
||||
</div>
|
||||
)}
|
||||
{/* <div id="gameBtn" className="TopButton" onClick={() => this.setState({ gameDownloadsOpen: !this.state.gameDownloadsOpen })}>
|
||||
<img src={gameBtn} alt="game" />
|
||||
</div> */}
|
||||
</TopBar>
|
||||
|
||||
<RightBar />
|
||||
|
||||
<NewsSection />
|
||||
|
||||
{
|
||||
// Extras section
|
||||
this.state.extrasOpen && (
|
||||
<ExtrasMenu closeFn={() => this.setState({ extrasOpen: false })} playGame={this.state.playGame}>
|
||||
Yo
|
||||
</ExtrasMenu>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
// Mini downloads section
|
||||
this.state.miniDownloadsOpen ? (
|
||||
<div className="MiniDownloads" id="miniDownloadContainer">
|
||||
<MiniDialog
|
||||
title="Downloads"
|
||||
closeFn={() => {
|
||||
this.setState({ miniDownloadsOpen: false })
|
||||
}}
|
||||
>
|
||||
<DownloadList downloadManager={this.props.downloadHandler} />
|
||||
</MiniDialog>
|
||||
<div className="arrow-down"></div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Download menu
|
||||
this.state.downloadsOpen ? (
|
||||
<Downloads
|
||||
downloadManager={this.props.downloadHandler}
|
||||
closeFn={() => this.setState({ downloadsOpen: false })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Options menu
|
||||
this.state.optionsOpen ? (
|
||||
<Options
|
||||
downloadManager={this.props.downloadHandler}
|
||||
closeFn={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Game downloads menu
|
||||
this.state.gameDownloadsOpen ? (
|
||||
<Game
|
||||
downloadManager={this.props.downloadHandler}
|
||||
closeFn={() => this.setState({ gameDownloadsOpen: false })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
<div className="BottomSection" id="bottomSectionContainer">
|
||||
<ServerLaunchSection openExtras={this.openExtrasMenu} />
|
||||
|
||||
<div
|
||||
id="DownloadProgress"
|
||||
onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })}
|
||||
>
|
||||
{this.state.isDownloading ? <MainProgressBar downloadManager={this.props.downloadHandler} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
41
src/ui/Mods.css
Normal file
41
src/ui/Mods.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.Mods {
|
||||
backdrop-filter: blur(10px);
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Stuff for the top bar progress bar */
|
||||
.TopDownloads {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 35.5%;
|
||||
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.TopDownloads .ProgressBar {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.ModMenu {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.ModMenu .BigButton {
|
||||
font-size: 16px;
|
||||
padding: 6px 30px;
|
||||
}
|
||||
|
||||
.ModDownloadList {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.ModDownloadItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
margin: 10px;
|
||||
}
|
||||
169
src/ui/Mods.tsx
Normal file
169
src/ui/Mods.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import React from 'react'
|
||||
import DownloadHandler from '../utils/download'
|
||||
import { getModDownload, ModData } from '../utils/gamebanana'
|
||||
import { getModsFolder } from '../utils/mods'
|
||||
import { unzip } from '../utils/zipUtils'
|
||||
import ProgressBar from './components/common/MainProgressBar'
|
||||
import { ModHeader } from './components/mods/ModHeader'
|
||||
import { ModList } from './components/mods/ModList'
|
||||
import TopBar from './components/TopBar'
|
||||
|
||||
import './Mods.css'
|
||||
import Back from '../resources/icons/back.svg'
|
||||
import Menu from './components/menu/Menu'
|
||||
import BigButton from './components/common/BigButton'
|
||||
import Tr from '../utils/language'
|
||||
|
||||
interface IProps {
|
||||
downloadHandler: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isDownloading: boolean
|
||||
category: string
|
||||
downloadList: { name: string; url: string; mod: ModData }[] | null
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{
|
||||
name: 'ripe',
|
||||
title: 'Hot',
|
||||
},
|
||||
{
|
||||
name: 'new',
|
||||
title: 'New',
|
||||
},
|
||||
{
|
||||
name: 'installed',
|
||||
title: 'Installed',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Mods currently install into folder labelled with their GB ID
|
||||
*
|
||||
* @TODO Categorizaiton/sorting (by likes, views, etc)
|
||||
*/
|
||||
export class Mods extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isDownloading: false,
|
||||
category: '',
|
||||
downloadList: null,
|
||||
}
|
||||
|
||||
this.setCategory = this.setCategory.bind(this)
|
||||
this.addDownload = this.addDownload.bind(this)
|
||||
}
|
||||
|
||||
async addDownload(mod: ModData) {
|
||||
const dlLinks = await getModDownload(String(mod.id))
|
||||
|
||||
// Not gonna bother allowing sorting for now
|
||||
const firstLink = dlLinks[0].downloadUrl
|
||||
const fileExt = firstLink.split('.').pop()
|
||||
|
||||
const modName = `${mod.id}.${fileExt}`
|
||||
|
||||
if (dlLinks.length === 0) return
|
||||
|
||||
// If there is one download we don't care to choose
|
||||
if (dlLinks.length === 1) {
|
||||
this.downloadMod(firstLink, modName, mod)
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
downloadList: dlLinks.map((link) => ({
|
||||
name: link.filename,
|
||||
url: link.downloadUrl,
|
||||
mod: mod,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
async downloadMod(link: string, modName: string, mod: ModData) {
|
||||
const modFolder = await getModsFolder()
|
||||
const path = `${modFolder}/${modName}`
|
||||
|
||||
if (!modFolder) return
|
||||
|
||||
this.props.downloadHandler.addDownload(link, path, async () => {
|
||||
const unzipRes = await unzip(path, modFolder, false, true)
|
||||
|
||||
// Write a modinfo.json file
|
||||
invoke('write_file', {
|
||||
path: `${unzipRes.new_folder}/modinfo.json`,
|
||||
contents: JSON.stringify(mod),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async setCategory(value: string) {
|
||||
this.setState(
|
||||
{
|
||||
category: value,
|
||||
},
|
||||
this.forceUpdate
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="Mods">
|
||||
<TopBar>
|
||||
<div
|
||||
id="backbtn"
|
||||
className="TopButton"
|
||||
onClick={() => {
|
||||
// Create and dispatch a custom "changePage" event
|
||||
const event = new CustomEvent('changePage', { detail: 'main' })
|
||||
window.dispatchEvent(event)
|
||||
}}
|
||||
>
|
||||
<img src={Back} alt="back" />
|
||||
</div>
|
||||
</TopBar>
|
||||
|
||||
{this.state.downloadList && (
|
||||
<Menu className="ModMenu" heading="Links" closeFn={() => this.setState({ downloadList: null })}>
|
||||
<div className="ModDownloadList">
|
||||
{this.state.downloadList.map((o) => {
|
||||
return (
|
||||
<div className="ModDownloadItem" key={o.name}>
|
||||
<div className="ModDownloadName">{o.name}</div>
|
||||
<BigButton
|
||||
id={o.url}
|
||||
onClick={() => {
|
||||
const fileExt = o.url.split('.').pop()
|
||||
const modName = `${o.mod.id}.${fileExt}`
|
||||
|
||||
this.downloadMod(o.url, modName, o.mod)
|
||||
this.setState({
|
||||
downloadList: null,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
<div className="TopDownloads">
|
||||
<ProgressBar downloadManager={this.props.downloadHandler} withStats={false} />
|
||||
</div>
|
||||
|
||||
<ModHeader onChange={this.setCategory} headers={headers} defaultHeader={'ripe'} />
|
||||
|
||||
<ModList key={this.state.category} mode={this.state.category} addDownload={this.addDownload} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import Close from '../../resources/icons/close.svg'
|
||||
import './MiniDialog.css'
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode[] | React.ReactNode;
|
||||
title?: string;
|
||||
closeable?: boolean;
|
||||
closeFn: () => void;
|
||||
children: React.ReactNode[] | React.ReactNode
|
||||
title?: string
|
||||
closeable?: boolean
|
||||
closeFn: () => void
|
||||
}
|
||||
|
||||
export default class MiniDialog extends React.Component<IProps, never> {
|
||||
@@ -33,13 +33,12 @@ export default class MiniDialog extends React.Component<IProps, never> {
|
||||
render() {
|
||||
return (
|
||||
<div className="MiniDialog" id="miniDialogContainer">
|
||||
{
|
||||
this.props.closeable !== undefined && this.props.closeable ?
|
||||
{this.props.closeable !== undefined && this.props.closeable ? (
|
||||
<div className="MiniDialogTop" id="miniDialogContainerTop" onClick={this.props.closeFn}>
|
||||
<span>{this.props?.title}</span>
|
||||
<img src={Close} className="MiniDialogClose" id="miniDialogButtonClose" />
|
||||
</div> : null
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="MiniDialogInner" id="miniDialogContent">
|
||||
{this.props.children}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
position: absolute;
|
||||
transform: translate(0%, 0%);
|
||||
|
||||
display:flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@@ -36,13 +36,13 @@
|
||||
filter: invert(75%) sepia(0%) saturate(100%) hue-rotate(0deg) brightness(100%) contrast(100%);
|
||||
}
|
||||
|
||||
@media(max-height: 580px) {
|
||||
@media (max-height: 580px) {
|
||||
.RightBar {
|
||||
height: calc(100vh - 180px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-height: 500px) {
|
||||
@media (max-height: 500px) {
|
||||
.RightBar {
|
||||
height: calc(100vh - 170px);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,12 @@
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.AkebiIcon,
|
||||
#ExtrasMenuButton {
|
||||
width: 5%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.ExtrasIcon,
|
||||
.ServerIcon {
|
||||
height: 20px;
|
||||
filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%);
|
||||
|
||||
@@ -8,33 +8,39 @@ import { translate } from '../../utils/language'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
import Server from '../../resources/icons/server.svg'
|
||||
import Akebi from '../../resources/icons/akebi.svg'
|
||||
import Plus from '../../resources/icons/plus.svg'
|
||||
|
||||
import './ServerLaunchSection.css'
|
||||
import {dataDir} from '@tauri-apps/api/path'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { getGameExecutable } from '../../utils/game'
|
||||
import { patchGame, unpatchGame } from '../../utils/metadata'
|
||||
|
||||
interface IState {
|
||||
grasscutterEnabled: boolean;
|
||||
buttonLabel: string;
|
||||
checkboxLabel: string;
|
||||
ip: string;
|
||||
port: string;
|
||||
|
||||
ipPlaceholder: string;
|
||||
portPlaceholder: string;
|
||||
|
||||
portHelpText: string;
|
||||
|
||||
httpsLabel: string;
|
||||
httpsEnabled: boolean;
|
||||
|
||||
swag: boolean;
|
||||
interface IProps {
|
||||
openExtras: (playGame: () => void) => void
|
||||
}
|
||||
|
||||
export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
constructor(props: {}) {
|
||||
interface IState {
|
||||
grasscutterEnabled: boolean
|
||||
buttonLabel: string
|
||||
checkboxLabel: string
|
||||
ip: string
|
||||
port: string
|
||||
|
||||
ipPlaceholder: string
|
||||
portPlaceholder: string
|
||||
|
||||
portHelpText: string
|
||||
|
||||
httpsLabel: string
|
||||
httpsEnabled: boolean
|
||||
|
||||
swag: boolean
|
||||
akebiSet: boolean
|
||||
migotoSet: boolean
|
||||
}
|
||||
|
||||
export default class ServerLaunchSection extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
@@ -48,12 +54,13 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
portHelpText: '',
|
||||
httpsLabel: '',
|
||||
httpsEnabled: false,
|
||||
swag: false
|
||||
swag: false,
|
||||
akebiSet: false,
|
||||
migotoSet: false,
|
||||
}
|
||||
|
||||
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
|
||||
this.playGame = this.playGame.bind(this)
|
||||
this.launchAkebi = this.launchAkebi.bind(this)
|
||||
this.setIp = this.setIp.bind(this)
|
||||
this.setPort = this.setPort.bind(this)
|
||||
this.toggleHttps = this.toggleHttps.bind(this)
|
||||
@@ -73,7 +80,9 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
portHelpText: await translate('help.port_help_text'),
|
||||
httpsLabel: await translate('main.https_enable'),
|
||||
httpsEnabled: config.https_enabled || false,
|
||||
swag: config.swag_mode || false
|
||||
swag: config.swag_mode || false,
|
||||
akebiSet: config.akebi_path !== '',
|
||||
migotoSet: config.migoto_path !== '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,7 +93,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
|
||||
// Set state as well
|
||||
this.setState({
|
||||
grasscutterEnabled: config.toggle_grasscutter
|
||||
grasscutterEnabled: config.toggle_grasscutter,
|
||||
})
|
||||
|
||||
await saveConfig(config)
|
||||
@@ -93,19 +102,21 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
async playGame(exe?: string, proc_name?: string) {
|
||||
const config = await getConfig()
|
||||
|
||||
if(!await getGameExecutable()) {
|
||||
if (!(await getGameExecutable())) {
|
||||
alert('Game executable not set!')
|
||||
return
|
||||
}
|
||||
|
||||
// Connect to proxy
|
||||
if (config.toggle_grasscutter) {
|
||||
if (config.patch_metadata) {
|
||||
const patched = await patchGame()
|
||||
|
||||
if (!patched) {
|
||||
alert('Could not patch game!')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const game_exe = await getGameExecutable()
|
||||
|
||||
@@ -113,14 +124,18 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
await setConfigOption('last_ip', this.state.ip)
|
||||
await setConfigOption('last_port', this.state.port)
|
||||
|
||||
// Set IP
|
||||
await invoke('set_proxy_addr', { addr: (this.state.httpsEnabled ? 'https':'http') + '://' + this.state.ip + ':' + this.state.port })
|
||||
await invoke('enable_process_watcher', {
|
||||
process: proc_name || game_exe
|
||||
process: proc_name || game_exe,
|
||||
})
|
||||
|
||||
if (config.use_internal_proxy) {
|
||||
// Set IP
|
||||
await invoke('set_proxy_addr', {
|
||||
addr: (this.state.httpsEnabled ? 'https' : 'http') + '://' + this.state.ip + ':' + this.state.port,
|
||||
})
|
||||
// Connect to proxy
|
||||
await invoke('connect', { port: 8365, certificatePath: await dataDir() + '\\cultivation\\ca' })
|
||||
await invoke('connect', { port: 8365, certificatePath: (await dataDir()) + '\\cultivation\\ca' })
|
||||
}
|
||||
|
||||
// Open server as well if the options are set
|
||||
if (config.grasscutter_with_game) {
|
||||
@@ -132,24 +147,26 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
await invoke('run_jar', {
|
||||
path: config.grasscutter_path,
|
||||
executeIn: jarFolder,
|
||||
javaPath: config.java_path || ''
|
||||
javaPath: config.java_path || '',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const unpatched = await unpatchGame()
|
||||
|
||||
if (!unpatched) {
|
||||
alert(`Could not unpatch game, aborting launch! (You can find your metadata backup in ${await dataDir()}\\cultivation\\)`)
|
||||
alert(
|
||||
`Could not unpatch game, aborting launch! (You can find your metadata backup in ${await dataDir()}\\cultivation\\)`
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the program
|
||||
const gameExists = await invoke('dir_exists', {
|
||||
path: exe || config.game_install_path
|
||||
path: exe || config.game_install_path,
|
||||
})
|
||||
|
||||
if (gameExists) await invoke('run_program', { path: exe || config.game_install_path })
|
||||
if (gameExists) await invoke('run_program_relative', { path: exe || config.game_install_path })
|
||||
else alert('Game not found! At: ' + (exe || config.game_install_path))
|
||||
}
|
||||
|
||||
@@ -170,29 +187,19 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
await invoke('run_jar', {
|
||||
path: config.grasscutter_path,
|
||||
executeIn: jarFolder,
|
||||
javaPath: config.java_path || ''
|
||||
javaPath: config.java_path || '',
|
||||
})
|
||||
}
|
||||
|
||||
async launchAkebi() {
|
||||
const config = await getConfig()
|
||||
|
||||
// Get game exe from game path, so we can watch it
|
||||
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
|
||||
const gameExec = pathArr[pathArr.length - 1]
|
||||
|
||||
await this.playGame(config.akebi_path, gameExec)
|
||||
}
|
||||
|
||||
setIp(text: string) {
|
||||
this.setState({
|
||||
ip: text
|
||||
ip: text,
|
||||
})
|
||||
}
|
||||
|
||||
setPort(text: string) {
|
||||
this.setState({
|
||||
port: text
|
||||
port: text,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -203,7 +210,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
|
||||
// Set state as well
|
||||
this.setState({
|
||||
httpsEnabled: config.https_enabled
|
||||
httpsEnabled: config.https_enabled,
|
||||
})
|
||||
|
||||
await saveConfig(config)
|
||||
@@ -213,34 +220,54 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
return (
|
||||
<div id="playButton">
|
||||
<div id="serverControls">
|
||||
<Checkbox id="enableGC" label={this.state.checkboxLabel} onChange={this.toggleGrasscutter} checked={this.state.grasscutterEnabled}/>
|
||||
<Checkbox
|
||||
id="enableGC"
|
||||
label={this.state.checkboxLabel}
|
||||
onChange={this.toggleGrasscutter}
|
||||
checked={this.state.grasscutterEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
this.state.grasscutterEnabled && (
|
||||
{this.state.grasscutterEnabled && (
|
||||
<div>
|
||||
<div className="ServerConfig" id="serverConfigContainer">
|
||||
<TextInput id="ip" key="ip" placeholder={this.state.ipPlaceholder} onChange={this.setIp} initalValue={this.state.ip} />
|
||||
<TextInput style={{
|
||||
<TextInput
|
||||
id="ip"
|
||||
key="ip"
|
||||
placeholder={this.state.ipPlaceholder}
|
||||
onChange={this.setIp}
|
||||
initalValue={this.state.ip}
|
||||
/>
|
||||
<TextInput
|
||||
style={{
|
||||
width: '10%',
|
||||
}} id="port" key="port" placeholder={this.state.portPlaceholder} onChange={this.setPort} initalValue={this.state.port} />
|
||||
}}
|
||||
id="port"
|
||||
key="port"
|
||||
placeholder={this.state.portPlaceholder}
|
||||
onChange={this.setPort}
|
||||
initalValue={this.state.port}
|
||||
/>
|
||||
<HelpButton contents={this.state.portHelpText} />
|
||||
<Checkbox id="httpsEnable" label={this.state.httpsLabel} onChange={this.toggleHttps} checked={this.state.httpsEnabled} />
|
||||
<Checkbox
|
||||
id="httpsEnable"
|
||||
label={this.state.httpsLabel}
|
||||
onChange={this.toggleHttps}
|
||||
checked={this.state.httpsEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
)}
|
||||
|
||||
<div className="ServerLaunchButtons" id="serverLaunchContainer">
|
||||
<BigButton onClick={this.playGame} id="officialPlay">{this.state.buttonLabel}</BigButton>
|
||||
{
|
||||
this.state.swag && (
|
||||
<BigButton onClick={this.launchAkebi} id="akebiLaunch">
|
||||
<img className="AkebiIcon" id="akebiIcon" src={Akebi} />
|
||||
<BigButton onClick={this.playGame} id="officialPlay">
|
||||
{this.state.buttonLabel}
|
||||
</BigButton>
|
||||
)
|
||||
}
|
||||
{this.state.swag && (
|
||||
<BigButton onClick={() => this.props.openExtras(this.playGame)} id="ExtrasMenuButton">
|
||||
<img className="ExtrasIcon" id="extrasIcon" src={Plus} />
|
||||
</BigButton>
|
||||
)}
|
||||
<BigButton onClick={this.launchServer} id="serverLaunch">
|
||||
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
|
||||
</BigButton>
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import React from 'react'
|
||||
import { app } from '@tauri-apps/api'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import closeIcon from '../../resources/icons/close.svg'
|
||||
import minIcon from '../../resources/icons/min.svg'
|
||||
import cogBtn from '../../resources/icons/cog.svg'
|
||||
import downBtn from '../../resources/icons/download.svg'
|
||||
|
||||
import { getConfig, setConfigOption } from '../../utils/configuration'
|
||||
import Tr from '../../utils/language'
|
||||
|
||||
import './TopBar.css'
|
||||
import { getConfig, setConfigOption } from '../../utils/configuration'
|
||||
import closeIcon from '../../resources/icons/close.svg'
|
||||
import minIcon from '../../resources/icons/min.svg'
|
||||
|
||||
interface IProps {
|
||||
optFunc: () => void;
|
||||
downFunc: () => void;
|
||||
gameFunc: () => void;
|
||||
children?: React.ReactNode | React.ReactNode[]
|
||||
}
|
||||
|
||||
interface IState {
|
||||
version: string;
|
||||
clicks: number;
|
||||
intv: NodeJS.Timeout | null;
|
||||
version: string
|
||||
clicks: number
|
||||
intv: NodeJS.Timeout | null
|
||||
}
|
||||
|
||||
export default class TopBar extends React.Component<IProps, IState> {
|
||||
@@ -30,7 +25,7 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
this.state = {
|
||||
version: '0.0.0',
|
||||
clicks: 0,
|
||||
intv: null
|
||||
intv: null,
|
||||
}
|
||||
|
||||
this.activateClick = this.activateClick.bind(this)
|
||||
@@ -59,7 +54,7 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
setTimeout(() => {
|
||||
// Gotta clear it so it goes back to regular colors
|
||||
this.setState({
|
||||
clicks: 0
|
||||
clicks: 0,
|
||||
})
|
||||
}, 600)
|
||||
|
||||
@@ -75,7 +70,7 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
if (this.state.clicks < 3) {
|
||||
this.setState({
|
||||
clicks: this.state.clicks + 1,
|
||||
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500)
|
||||
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -89,34 +84,28 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
<span data-tauri-drag-region>
|
||||
<Tr text="main.title" />
|
||||
</span>
|
||||
<span data-tauri-drag-region id="version">{this.state?.version}</span>
|
||||
<span data-tauri-drag-region id="version">
|
||||
{this.state?.version}
|
||||
</span>
|
||||
</div>
|
||||
{
|
||||
/**
|
||||
{/**
|
||||
* HEY YOU
|
||||
*
|
||||
* If you're looking at the source code to find the swag mode thing, that's okay! If you're not, move along...
|
||||
* Just do me a favor and don't go telling everyone about how you found it. If you are just helping someone who
|
||||
* for some reason needs it, that's fine, but not EVERYONE needs it, which is why it exists in the first place.
|
||||
*/
|
||||
}
|
||||
<div id="unassumingButton" className={this.state.clicks === 2 ? 'spin' : ''} onClick={this.activateClick}>?</div>
|
||||
*/}
|
||||
<div id="unassumingButton" className={this.state.clicks === 2 ? 'spin' : ''} onClick={this.activateClick}>
|
||||
?
|
||||
</div>
|
||||
<div className="TopBtns" id="topBarButtonContainer">
|
||||
<div id="closeBtn" onClick={this.handleClose} className='TopButton'>
|
||||
<div id="closeBtn" onClick={this.handleClose} className="TopButton">
|
||||
<img src={closeIcon} alt="close" />
|
||||
</div>
|
||||
<div id="minBtn" onClick={this.handleMinimize} className='TopButton'>
|
||||
<div id="minBtn" onClick={this.handleMinimize} className="TopButton">
|
||||
<img src={minIcon} alt="minimize" />
|
||||
</div>
|
||||
<div id="settingsBtn" onClick={this.props.optFunc} className='TopButton'>
|
||||
<img src={cogBtn} alt="settings" />
|
||||
</div>
|
||||
<div id="downloadsBtn" className='TopButton' onClick={this.props.downFunc}>
|
||||
<img src={downBtn} alt="downloads" />
|
||||
</div>
|
||||
{/* <div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>
|
||||
<img src={gameBtn} alt="game" />
|
||||
</div> */}
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react'
|
||||
import './BigButton.css'
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
onClick: () => unknown;
|
||||
id: string;
|
||||
disabled?: boolean;
|
||||
children: React.ReactNode
|
||||
onClick: () => unknown
|
||||
id: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface IState {
|
||||
disabled?: boolean;
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default class BigButton extends React.Component<IProps, IState> {
|
||||
@@ -17,7 +17,7 @@ export default class BigButton extends React.Component<IProps, IState> {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
disabled: this.props.disabled
|
||||
disabled: this.props.disabled,
|
||||
}
|
||||
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
@@ -25,7 +25,7 @@ export default class BigButton extends React.Component<IProps, IState> {
|
||||
|
||||
static getDerivedStateFromProps(props: IProps, _state: IState) {
|
||||
return {
|
||||
disabled: props.disabled
|
||||
disabled: props.disabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ export default class BigButton extends React.Component<IProps, IState> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={'BigButton ' + (this.state.disabled ? 'disabled' : '')} onClick={this.handleClick} id={this.props.id}>
|
||||
<div
|
||||
className={'BigButton ' + (this.state.disabled ? 'disabled' : '')}
|
||||
onClick={this.handleClick}
|
||||
id={this.props.id}
|
||||
>
|
||||
<div className="BigButtonText">{this.props.children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.Checkbox input[type="checkbox"] {
|
||||
.Checkbox input[type='checkbox'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
.CheckboxDisplay img {
|
||||
height: 100%;
|
||||
filter: invert(99%) sepia(0%) saturate(1188%) hue-rotate(186deg) brightness(97%) contrast(67%)
|
||||
filter: invert(99%) sepia(0%) saturate(1188%) hue-rotate(186deg) brightness(97%) contrast(67%);
|
||||
}
|
||||
|
||||
.Checkbox label {
|
||||
|
||||
@@ -4,10 +4,10 @@ import checkmark from '../../../resources/icons/check.svg'
|
||||
import './Checkbox.css'
|
||||
|
||||
interface IProps {
|
||||
label?: string,
|
||||
checked: boolean,
|
||||
onChange: () => void,
|
||||
id: string
|
||||
label?: string
|
||||
checked: boolean
|
||||
onChange: () => void
|
||||
id?: string
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -19,14 +19,14 @@ export default class Checkbox extends React.Component<IProps, IState> {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
checked: props.checked
|
||||
checked: props.checked,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: IProps, state: IState) {
|
||||
if (props.checked !== state.checked) {
|
||||
return {
|
||||
checked: props.checked
|
||||
checked: props.checked,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,9 @@ export default class Checkbox extends React.Component<IProps, IState> {
|
||||
render() {
|
||||
return (
|
||||
<div className="Checkbox">
|
||||
<input type='checkbox' id={this.props.id} checked={this.state.checked} onChange={this.handleChange} />
|
||||
<input type="checkbox" id={this.props.id} checked={this.state.checked} onChange={this.handleChange} />
|
||||
<label htmlFor={this.props.id}>
|
||||
<div className="CheckboxDisplay">
|
||||
{this.state.checked ? <img src={checkmark} alt='Checkmark' /> : null}
|
||||
</div>
|
||||
<div className="CheckboxDisplay">{this.state.checked ? <img src={checkmark} alt="Checkmark" /> : null}</div>
|
||||
<span>{this.props.label || ''}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ interface IProps {
|
||||
readonly?: boolean
|
||||
placeholder?: string
|
||||
folder?: boolean
|
||||
customClearBehaviour?: () => void,
|
||||
customClearBehaviour?: () => void
|
||||
openFolder?: string
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class DirInput extends React.Component<IProps, IState> {
|
||||
this.state = {
|
||||
value: props.value || '',
|
||||
placeholder: this.props.placeholder || 'Select file or folder...',
|
||||
folder: this.props.folder || false
|
||||
folder: this.props.folder || false,
|
||||
}
|
||||
|
||||
this.handleIconClick = this.handleIconClick.bind(this)
|
||||
@@ -54,8 +54,8 @@ export default class DirInput extends React.Component<IProps, IState> {
|
||||
async componentDidMount() {
|
||||
if (!this.props.placeholder) {
|
||||
const translation = await translate('components.select_file')
|
||||
this.setState( {
|
||||
placeholder: translation
|
||||
this.setState({
|
||||
placeholder: translation,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -65,15 +65,12 @@ export default class DirInput extends React.Component<IProps, IState> {
|
||||
|
||||
if (this.state.folder) {
|
||||
path = await open({
|
||||
directory: true
|
||||
directory: true,
|
||||
})
|
||||
} else {
|
||||
console.log(this.props.openFolder)
|
||||
path = await open({
|
||||
filters: [
|
||||
{ name: 'Files', extensions: this.props.extensions || ['*'] }
|
||||
],
|
||||
defaultPath: this.props.openFolder
|
||||
filters: [{ name: 'Files', extensions: this.props.extensions || ['*'] }],
|
||||
defaultPath: this.props.openFolder,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,7 +78,7 @@ export default class DirInput extends React.Component<IProps, IState> {
|
||||
if (!path) return
|
||||
|
||||
this.setState({
|
||||
value: path
|
||||
value: path,
|
||||
})
|
||||
|
||||
if (this.props.onChange) this.props.onChange(path)
|
||||
@@ -89,12 +86,13 @@ export default class DirInput extends React.Component<IProps, IState> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='DirInput'>
|
||||
<div className="DirInput">
|
||||
<TextInput
|
||||
value={this.state.value}
|
||||
placeholder={this.state.placeholder}
|
||||
clearable={this.props.clearable !== undefined ? this.props.clearable : true}
|
||||
readOnly={this.props.readonly !== undefined ? this.props.readonly : true } onChange={(text: string) => {
|
||||
readOnly={this.props.readonly !== undefined ? this.props.readonly : true}
|
||||
onChange={(text: string) => {
|
||||
this.setState({ value: text })
|
||||
|
||||
if (this.props.onChange) this.props.onChange(text)
|
||||
|
||||
@@ -5,7 +5,7 @@ import DownloadSection from './DownloadSection'
|
||||
import './DownloadList.css'
|
||||
|
||||
interface IProps {
|
||||
downloadManager: DownloadHandler;
|
||||
downloadManager: DownloadHandler
|
||||
}
|
||||
|
||||
export default class DownloadList extends React.Component<IProps, never> {
|
||||
@@ -16,17 +16,14 @@ export default class DownloadList extends React.Component<IProps, never> {
|
||||
render() {
|
||||
const list = this.props.downloadManager.getDownloads().map((download) => {
|
||||
return (
|
||||
<DownloadSection key={download.path} downloadName={download.path} downloadManager={this.props.downloadManager} />
|
||||
<DownloadSection
|
||||
key={download.path}
|
||||
downloadName={download.path}
|
||||
downloadManager={this.props.downloadManager}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<div className="DownloadList">
|
||||
{
|
||||
list.length > 0 ? list : 'No downloads present'
|
||||
}
|
||||
</div>
|
||||
)
|
||||
return <div className="DownloadList">{list.length > 0 ? list : 'No downloads present'}</div>
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import ProgressBar from './ProgressBar'
|
||||
import './DownloadSection.css'
|
||||
|
||||
interface IProps {
|
||||
downloadManager: DownloadHandler;
|
||||
downloadName: string;
|
||||
downloadManager: DownloadHandler
|
||||
downloadName: string
|
||||
}
|
||||
|
||||
export default class DownloadSection extends React.Component<IProps, never> {
|
||||
|
||||
@@ -2,52 +2,31 @@ import React from 'react'
|
||||
|
||||
import './HelpButton.css'
|
||||
import Help from '../../../resources/icons/help.svg'
|
||||
import MiniDialog from '../MiniDialog'
|
||||
import { translate } from '../../../utils/language'
|
||||
|
||||
interface IProps {
|
||||
children?: React.ReactNode[] | React.ReactNode;
|
||||
children?: React.ReactNode[] | React.ReactNode
|
||||
contents?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
interface IState {
|
||||
opened: boolean
|
||||
}
|
||||
|
||||
export default class HelpButton extends React.Component<IProps, IState> {
|
||||
export default class HelpButton extends React.Component<IProps, never> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
opened: false
|
||||
this.showAlert = this.showAlert.bind(this)
|
||||
}
|
||||
|
||||
this.setOpen = this.setOpen.bind(this)
|
||||
this.setClosed = this.setClosed.bind(this)
|
||||
}
|
||||
|
||||
setOpen() {
|
||||
this.setState({ opened: true })
|
||||
}
|
||||
|
||||
setClosed() {
|
||||
this.setState({ opened: false })
|
||||
async showAlert() {
|
||||
if (this.props.contents) alert(await translate(this.props.contents))
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="HelpSection">
|
||||
<div className="HelpButton" onMouseEnter={this.setOpen} onMouseLeave={this.setClosed}>
|
||||
<div className="HelpButton" onClick={this.showAlert}>
|
||||
<img src={Help} />
|
||||
</div>
|
||||
|
||||
<div className="HelpContents" style={{
|
||||
display: this.state.opened ? 'block' : 'none'
|
||||
}}>
|
||||
<MiniDialog closeFn={this.setClosed}>
|
||||
{this.props.contents || this.props.children}
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@ import Tr from '../../../utils/language'
|
||||
import './ProgressBar.css'
|
||||
|
||||
interface IProps {
|
||||
downloadManager: DownloadHandler,
|
||||
downloadManager: DownloadHandler
|
||||
withStats?: boolean
|
||||
}
|
||||
|
||||
interface IState {
|
||||
average: number,
|
||||
files: number,
|
||||
extracting: number,
|
||||
total: number,
|
||||
speed: string,
|
||||
average: number
|
||||
files: number
|
||||
extracting: number
|
||||
total: number
|
||||
speed: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
files,
|
||||
extracting,
|
||||
total: totalSize,
|
||||
speed: '0 B/s'
|
||||
speed: '0 B/s',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +52,12 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
return (
|
||||
<div className="MainProgressBarWrapper">
|
||||
<div className="ProgressBar">
|
||||
<div className="InnerProgress" style={{
|
||||
<div
|
||||
className="InnerProgress"
|
||||
style={{
|
||||
width: `${(() => {
|
||||
// Handles no files downloading
|
||||
if (this.state.files === 0) {
|
||||
if (this.state.files === 0 || this.state.average >= 100) {
|
||||
return '100'
|
||||
}
|
||||
|
||||
@@ -64,14 +67,17 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
|
||||
return this.state.average
|
||||
})()}%`,
|
||||
}}></div>
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
{(this.props.withStats === undefined || this.props.withStats) && (
|
||||
<div className="MainProgressText">
|
||||
<Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed})
|
||||
<br />
|
||||
<Tr text="main.files_extracting" /> {this.state.extracting}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.ProgressBar, .InnerProgress {
|
||||
.ProgressBar,
|
||||
.InnerProgress {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ import DownloadHandler from '../../../utils/download'
|
||||
import { translate } from '../../../utils/language'
|
||||
|
||||
interface IProps {
|
||||
path: string,
|
||||
downloadManager: DownloadHandler,
|
||||
path: string
|
||||
downloadManager: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
progress: number,
|
||||
status: string,
|
||||
total: number,
|
||||
progress: number
|
||||
status: string
|
||||
total: number
|
||||
}
|
||||
|
||||
export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
@@ -36,7 +36,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
const prog = this.props.downloadManager.getDownloadProgress(this.props.path)
|
||||
this.setState({
|
||||
progress: prog?.progress || 0,
|
||||
status: await translate(`download_status.${prog?.status || 'stopped'}`) || 'stopped',
|
||||
status: (await translate(`download_status.${prog?.status || 'stopped'}`)) || 'stopped',
|
||||
total: prog?.total || 0,
|
||||
})
|
||||
|
||||
@@ -54,11 +54,15 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
render() {
|
||||
return (
|
||||
<div className="ProgressBarWrapper">
|
||||
<div style={{
|
||||
width: '80%'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '80%',
|
||||
}}
|
||||
>
|
||||
<div className="ProgressBar">
|
||||
<div className="InnerProgress" style={{
|
||||
<div
|
||||
className="InnerProgress"
|
||||
style={{
|
||||
width: `${(() => {
|
||||
// Handles files with content-lengths of 0
|
||||
if (this.state.status === 'finished') {
|
||||
@@ -69,9 +73,10 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
return '0'
|
||||
}
|
||||
|
||||
return this.state.progress / this.state.total * 100
|
||||
return (this.state.progress / this.state.total) * 100
|
||||
})()}%`,
|
||||
}}></div>
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="DownloadControls">
|
||||
<div onClick={this.stopDownload} className="downloadStop">
|
||||
@@ -80,9 +85,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ProgressText">
|
||||
{capitalize(this.state.status) || 'Waiting'}
|
||||
</div>
|
||||
<div className="ProgressText">{capitalize(this.state.status) || 'Waiting'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ import './TextInput.css'
|
||||
import Close from '../../../resources/icons/close.svg'
|
||||
|
||||
interface IProps {
|
||||
value?: string;
|
||||
initalValue?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (value: string) => void;
|
||||
readOnly?: boolean;
|
||||
id?: string;
|
||||
clearable?: boolean;
|
||||
customClearBehaviour?: () => void;
|
||||
style?: React.CSSProperties;
|
||||
value?: string
|
||||
initalValue?: string
|
||||
placeholder?: string
|
||||
onChange?: (value: string) => void
|
||||
readOnly?: boolean
|
||||
id?: string
|
||||
clearable?: boolean
|
||||
customClearBehaviour?: () => void
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -24,14 +24,14 @@ export default class TextInput extends React.Component<IProps, IState> {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
value: props.value || ''
|
||||
value: props.value || '',
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.initalValue) {
|
||||
this.setState({
|
||||
value: this.props.initalValue
|
||||
value: this.props.initalValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,13 +43,21 @@ export default class TextInput extends React.Component<IProps, IState> {
|
||||
render() {
|
||||
return (
|
||||
<div className="TextInputWrapper" style={this.props.style || {}}>
|
||||
<input id={this.props?.id} readOnly={this.props.readOnly || false} placeholder={this.props.placeholder || ''} className="TextInput" value={this.state.value} onChange={(e) => {
|
||||
<input
|
||||
id={this.props?.id}
|
||||
readOnly={this.props.readOnly || false}
|
||||
placeholder={this.props.placeholder || ''}
|
||||
className="TextInput"
|
||||
value={this.state.value}
|
||||
onChange={(e) => {
|
||||
this.setState({ value: e.target.value })
|
||||
if (this.props.onChange) this.props.onChange(e.target.value)
|
||||
}} />
|
||||
{
|
||||
this.props.clearable ?
|
||||
<div className="TextClear" onClick={() => {
|
||||
}}
|
||||
/>
|
||||
{this.props.clearable ? (
|
||||
<div
|
||||
className="TextClear"
|
||||
onClick={() => {
|
||||
// Run custom behaviour first
|
||||
if (this.props.customClearBehaviour) return this.props.customClearBehaviour()
|
||||
|
||||
@@ -58,10 +66,11 @@ export default class TextInput extends React.Component<IProps, IState> {
|
||||
if (this.props.onChange) this.props.onChange('')
|
||||
|
||||
this.forceUpdate()
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<img src={Close} className="TextInputClear" />
|
||||
</div> : null
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.Divider {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -17,11 +17,11 @@ const STABLE_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archiv
|
||||
const DEV_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip'
|
||||
const STABLE_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip'
|
||||
const DEV_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip'
|
||||
const RESOURCES_DOWNLOAD = 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/2.8/GrasscutterResources-2.8.zip'
|
||||
const RESOURCES_DOWNLOAD = 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/3.0/GrasscutterResources-3.0.zip'
|
||||
|
||||
interface IProps {
|
||||
closeFn: () => void;
|
||||
downloadManager: DownloadHandler;
|
||||
closeFn: () => void
|
||||
downloadManager: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -41,7 +41,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
resources_downloading: this.props.downloadManager.downloadingResources(),
|
||||
repo_downloading: this.props.downloadManager.downloadingRepo(),
|
||||
grasscutter_set: false,
|
||||
resources_exist: false
|
||||
resources_exist: false,
|
||||
}
|
||||
|
||||
this.getGrasscutterFolder = this.getGrasscutterFolder.bind(this)
|
||||
@@ -63,7 +63,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
if (!gc_path || gc_path === '') {
|
||||
this.setState({
|
||||
grasscutter_set: false,
|
||||
resources_exist: false
|
||||
resources_exist: false,
|
||||
})
|
||||
|
||||
return
|
||||
@@ -72,15 +72,17 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
const path = gc_path.substring(0, gc_path.lastIndexOf('\\'))
|
||||
|
||||
if (gc_path) {
|
||||
const resources_exist: boolean = await invoke('dir_exists', {
|
||||
path: path + '\\resources'
|
||||
}) as boolean && !(await invoke('dir_is_empty', {
|
||||
path: path + '\\resources'
|
||||
})) as boolean
|
||||
const resources_exist: boolean =
|
||||
((await invoke('dir_exists', {
|
||||
path: path + '\\resources',
|
||||
})) as boolean) &&
|
||||
(!(await invoke('dir_is_empty', {
|
||||
path: path + '\\resources',
|
||||
})) as boolean)
|
||||
|
||||
this.setState({
|
||||
grasscutter_set: gc_path !== '',
|
||||
resources_exist
|
||||
resources_exist,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -109,8 +111,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterStableRepo() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{
|
||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
|
||||
this.toggleButtons()
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
@@ -118,8 +121,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterDevRepo() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{
|
||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
|
||||
this.toggleButtons()
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
@@ -127,8 +131,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterStable() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', () =>{
|
||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
|
||||
this.toggleButtons
|
||||
})
|
||||
|
||||
// Also add repo download
|
||||
@@ -139,8 +144,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterLatest() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', () =>{
|
||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
|
||||
this.toggleButtons()
|
||||
})
|
||||
|
||||
// Also add repo download
|
||||
@@ -153,24 +159,25 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(RESOURCES_DOWNLOAD, folder + '\\resources.zip', async () => {
|
||||
// Delete the existing folder if it exists
|
||||
if (await invoke('dir_exists', {
|
||||
path: folder + '\\resources'
|
||||
})) {
|
||||
if (
|
||||
await invoke('dir_exists', {
|
||||
path: folder + '\\resources',
|
||||
})
|
||||
) {
|
||||
await invoke('dir_delete', {
|
||||
path: folder + '\\resources'
|
||||
path: folder + '\\resources',
|
||||
})
|
||||
}
|
||||
|
||||
await unzip(folder + '\\resources.zip', folder + '\\', () => {
|
||||
await unzip(folder + '\\resources.zip', folder + '\\', true)
|
||||
// Rename folder to resources
|
||||
invoke('rename', {
|
||||
path: folder + '\\Resources',
|
||||
newName: 'resources'
|
||||
newName: 'resources',
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
})
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
}
|
||||
@@ -190,32 +197,36 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
render() {
|
||||
return (
|
||||
<Menu closeFn={this.props.closeFn} className="Downloads" heading="Downloads">
|
||||
<div className='DownloadMenuSection' id="downloadMenuContainerGCStable">
|
||||
<div className='DownloadLabel' id="downloadMenuLabelGCStable">
|
||||
<Tr text={
|
||||
this.state.grasscutter_set ? 'downloads.grasscutter_stable' : 'downloads.grasscutter_stable_update'
|
||||
} />
|
||||
<HelpButton>
|
||||
<Tr text="help.gc_stable_jar" />
|
||||
</HelpButton>
|
||||
<div className="DownloadMenuSection" id="downloadMenuContainerGCStable">
|
||||
<div className="DownloadLabel" id="downloadMenuLabelGCStable">
|
||||
<Tr
|
||||
text={this.state.grasscutter_set ? 'downloads.grasscutter_stable' : 'downloads.grasscutter_stable_update'}
|
||||
/>
|
||||
<HelpButton contents="help.gc_stable_jar" />
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCStable">
|
||||
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterStable} id="grasscutterStableBtn" >
|
||||
<div className="DownloadValue" id="downloadMenuButtonGCStable">
|
||||
<BigButton
|
||||
disabled={this.state.grasscutter_downloading}
|
||||
onClick={this.downloadGrasscutterStable}
|
||||
id="grasscutterStableBtn"
|
||||
>
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='DownloadMenuSection' id="downloadMenuContainerGCDev">
|
||||
<div className='DownloadLabel' id="downloadMenuLabelGCDev">
|
||||
<Tr text={
|
||||
this.state.grasscutter_set ? 'downloads.grasscutter_latest' : 'downloads.grasscutter_latest_update'
|
||||
} />
|
||||
<HelpButton>
|
||||
<Tr text="help.gc_dev_jar" />
|
||||
</HelpButton>
|
||||
<div className="DownloadMenuSection" id="downloadMenuContainerGCDev">
|
||||
<div className="DownloadLabel" id="downloadMenuLabelGCDev">
|
||||
<Tr
|
||||
text={this.state.grasscutter_set ? 'downloads.grasscutter_latest' : 'downloads.grasscutter_latest_update'}
|
||||
/>
|
||||
<HelpButton contents="help.gc_dev_jar" />
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCDev">
|
||||
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterLatest} id="grasscutterLatestBtn" >
|
||||
<div className="DownloadValue" id="downloadMenuButtonGCDev">
|
||||
<BigButton
|
||||
disabled={this.state.grasscutter_downloading}
|
||||
onClick={this.downloadGrasscutterLatest}
|
||||
id="grasscutterLatestBtn"
|
||||
>
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
@@ -223,32 +234,44 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className='DownloadMenuSection' id="downloadMenuContainerGCStableData">
|
||||
<div className='DownloadLabel' id="downloadMenuLabelGCStableData">
|
||||
<Tr text={
|
||||
this.state.grasscutter_set ? 'downloads.grasscutter_stable_data' : 'downloads.grasscutter_stable_data_update'
|
||||
} />
|
||||
<HelpButton>
|
||||
<Tr text="help.gc_stable_data" />
|
||||
</HelpButton>
|
||||
<div className="DownloadMenuSection" id="downloadMenuContainerGCStableData">
|
||||
<div className="DownloadLabel" id="downloadMenuLabelGCStableData">
|
||||
<Tr
|
||||
text={
|
||||
this.state.grasscutter_set
|
||||
? 'downloads.grasscutter_stable_data'
|
||||
: 'downloads.grasscutter_stable_data_update'
|
||||
}
|
||||
/>
|
||||
<HelpButton contents="help.gc_stable_data" />
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCStableData">
|
||||
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterStableRepo" >
|
||||
<div className="DownloadValue" id="downloadMenuButtonGCStableData">
|
||||
<BigButton
|
||||
disabled={this.state.repo_downloading}
|
||||
onClick={this.downloadGrasscutterStableRepo}
|
||||
id="grasscutterStableRepo"
|
||||
>
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='DownloadMenuSection' id="downloadMenuContainerGCDevData">
|
||||
<div className='DownloadLabel' id="downloadMenuLabelGCDevData">
|
||||
<Tr text={
|
||||
this.state.grasscutter_set ? 'downloads.grasscutter_latest_data' : 'downloads.grasscutter_latest_data_update'
|
||||
} />
|
||||
<HelpButton>
|
||||
<Tr text="help.gc_dev_data" />
|
||||
</HelpButton>
|
||||
<div className="DownloadMenuSection" id="downloadMenuContainerGCDevData">
|
||||
<div className="DownloadLabel" id="downloadMenuLabelGCDevData">
|
||||
<Tr
|
||||
text={
|
||||
this.state.grasscutter_set
|
||||
? 'downloads.grasscutter_latest_data'
|
||||
: 'downloads.grasscutter_latest_data_update'
|
||||
}
|
||||
/>
|
||||
<HelpButton contents="help.gc_dev_data" />
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCDevData">
|
||||
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterDevRepo" >
|
||||
<div className="DownloadValue" id="downloadMenuButtonGCDevData">
|
||||
<BigButton
|
||||
disabled={this.state.repo_downloading}
|
||||
onClick={this.downloadGrasscutterStableRepo}
|
||||
id="grasscutterDevRepo"
|
||||
>
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
@@ -256,15 +279,17 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className='DownloadMenuSection' id="downloadMenuContainerResources">
|
||||
<div className='DownloadLabel' id="downloadMenuLabelResources">
|
||||
<div className="DownloadMenuSection" id="downloadMenuContainerResources">
|
||||
<div className="DownloadLabel" id="downloadMenuLabelResources">
|
||||
<Tr text="downloads.resources" />
|
||||
<HelpButton>
|
||||
<Tr text="help.resources" />
|
||||
</HelpButton>
|
||||
<HelpButton contents="help.resources" />
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonResources">
|
||||
<BigButton disabled={this.state.resources_downloading || !this.state.grasscutter_set || this.state.resources_exist} onClick={this.downloadResources} id="resourcesBtn" >
|
||||
<div className="DownloadValue" id="downloadMenuButtonResources">
|
||||
<BigButton
|
||||
disabled={this.state.resources_downloading || !this.state.grasscutter_set || this.state.resources_exist}
|
||||
onClick={this.downloadResources}
|
||||
id="resourcesBtn"
|
||||
>
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
|
||||
32
src/ui/components/menu/ExtrasMenu.css
Normal file
32
src/ui/components/menu/ExtrasMenu.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.ExtrasMenu {
|
||||
width: 20%;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.ExtrasMenu .MenuInner {
|
||||
justify-content: space-between;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.ExtrasMenuContent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ExtraItem {
|
||||
width: 80%;
|
||||
|
||||
padding: 6px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ExtraLaunch .BigButton {
|
||||
padding: 20px 50px;
|
||||
}
|
||||
179
src/ui/components/menu/ExtrasMenu.tsx
Normal file
179
src/ui/components/menu/ExtrasMenu.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React from 'react'
|
||||
import { getConfig, saveConfig } from '../../../utils/configuration'
|
||||
import Checkbox from '../common/Checkbox'
|
||||
import Menu from './Menu'
|
||||
|
||||
import './ExtrasMenu.css'
|
||||
import BigButton from '../common/BigButton'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import Tr from '../../../utils/language'
|
||||
import { getGameExecutable } from '../../../utils/game'
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode | React.ReactNode[]
|
||||
closeFn: () => void
|
||||
playGame: (exe?: string, proc_name?: string) => void
|
||||
}
|
||||
|
||||
interface IState {
|
||||
migoto?: string
|
||||
akebi?: string
|
||||
reshade?: string
|
||||
launch_migoto: boolean
|
||||
launch_akebi: boolean
|
||||
launch_reshade: boolean
|
||||
}
|
||||
|
||||
export class ExtrasMenu extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
launch_migoto: false,
|
||||
launch_akebi: false,
|
||||
launch_reshade: false,
|
||||
}
|
||||
|
||||
this.launchPreprograms = this.launchPreprograms.bind(this)
|
||||
this.toggleMigoto = this.toggleMigoto.bind(this)
|
||||
this.toggleAkebi = this.toggleAkebi.bind(this)
|
||||
this.toggleReshade = this.toggleReshade.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const config = await getConfig()
|
||||
|
||||
this.setState({
|
||||
migoto: config.migoto_path,
|
||||
akebi: config.akebi_path,
|
||||
reshade: config.reshade_path,
|
||||
launch_akebi: config?.last_extras?.akebi ?? false,
|
||||
launch_migoto: config?.last_extras?.migoto ?? false,
|
||||
launch_reshade: config?.last_extras?.reshade ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
async launchPreprograms() {
|
||||
const config = await getConfig()
|
||||
|
||||
config.last_extras = {
|
||||
migoto: this.state.launch_migoto,
|
||||
akebi: this.state.launch_akebi,
|
||||
reshade: this.state.launch_reshade,
|
||||
}
|
||||
|
||||
await saveConfig(config)
|
||||
|
||||
// Close menu
|
||||
this.props.closeFn()
|
||||
|
||||
// This injects independent of the game
|
||||
if (this.state.launch_migoto) {
|
||||
await this.launchMigoto()
|
||||
}
|
||||
|
||||
// This injects independent of the game
|
||||
if (this.state.launch_reshade) {
|
||||
await this.launchReshade()
|
||||
}
|
||||
|
||||
// This will launch the game
|
||||
if (this.state.launch_akebi) {
|
||||
await this.launchAkebi()
|
||||
|
||||
// This already launches the game
|
||||
return
|
||||
}
|
||||
|
||||
// Launch the game
|
||||
await this.props.playGame()
|
||||
}
|
||||
|
||||
async launchAkebi() {
|
||||
const config = await getConfig()
|
||||
|
||||
// Get game exe from game path, so we can watch it
|
||||
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
|
||||
const gameExec = pathArr[pathArr.length - 1]
|
||||
|
||||
await this.props.playGame(config.akebi_path, gameExec)
|
||||
}
|
||||
|
||||
async launchMigoto() {
|
||||
const config = await getConfig()
|
||||
|
||||
if (!config.migoto_path) return alert('Migoto not installed or set!')
|
||||
|
||||
await invoke('run_program_relative', { path: config.migoto_path })
|
||||
}
|
||||
|
||||
async launchReshade() {
|
||||
const config = await getConfig()
|
||||
|
||||
if (!config.reshade_path) return alert('Reshade not installed or set!')
|
||||
|
||||
await invoke('run_command', {
|
||||
program: config.reshade_path,
|
||||
args: [await getGameExecutable()],
|
||||
relative: true,
|
||||
})
|
||||
}
|
||||
|
||||
toggleMigoto() {
|
||||
this.setState({
|
||||
launch_migoto: !this.state.launch_migoto,
|
||||
})
|
||||
}
|
||||
|
||||
toggleAkebi() {
|
||||
this.setState({
|
||||
launch_akebi: !this.state.launch_akebi,
|
||||
})
|
||||
}
|
||||
|
||||
toggleReshade() {
|
||||
this.setState({
|
||||
launch_reshade: !this.state.launch_reshade,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Menu closeFn={this.props.closeFn} heading="Extras" className="ExtrasMenu">
|
||||
<div className="ExtrasMenuContent">
|
||||
{this.state.migoto && (
|
||||
<div className="ExtraItem">
|
||||
<div className="ExtraItemLabel">
|
||||
<Tr text="swag.migoto_name" />
|
||||
</div>
|
||||
<Checkbox id="MigotoCheckbox" checked={this.state.launch_migoto} onChange={this.toggleMigoto} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.akebi && (
|
||||
<div className="ExtraItem">
|
||||
<div className="ExtraItemLabel">
|
||||
<Tr text="swag.akebi_name" />
|
||||
</div>
|
||||
<Checkbox id="AkebiCheckbox" checked={this.state.launch_akebi} onChange={this.toggleAkebi} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.reshade && (
|
||||
<div className="ExtraItem">
|
||||
<div className="ExtraItemLabel">
|
||||
<Tr text="swag.reshade_name" />
|
||||
</div>
|
||||
<Checkbox id="ReshadeCheckbox" checked={this.state.launch_reshade} onChange={this.toggleReshade} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ExtraLaunch">
|
||||
<BigButton id="ExtraLaunch" onClick={this.launchPreprograms}>
|
||||
<Tr text="main.launch_button" />
|
||||
</BigButton>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import Menu from './Menu'
|
||||
import Tr, { translate } from '../../../utils/language'
|
||||
import { translate } from '../../../utils/language'
|
||||
import DownloadHandler from '../../../utils/download'
|
||||
|
||||
import './Game.css'
|
||||
@@ -12,14 +12,14 @@ import { unzip } from '../../../utils/zipUtils'
|
||||
const GAME_DOWNLOAD = ''
|
||||
|
||||
interface IProps {
|
||||
closeFn: () => void;
|
||||
downloadManager: DownloadHandler;
|
||||
closeFn: () => void
|
||||
downloadManager: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
gameDownloading: boolean;
|
||||
gameDownloadFolder: string;
|
||||
dirPlaceholder: string;
|
||||
gameDownloading: boolean
|
||||
gameDownloadFolder: string
|
||||
dirPlaceholder: string
|
||||
}
|
||||
|
||||
export default class Downloads extends React.Component<IProps, IState> {
|
||||
@@ -29,7 +29,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
this.state = {
|
||||
gameDownloading: false,
|
||||
gameDownloadFolder: '',
|
||||
dirPlaceholder: ''
|
||||
dirPlaceholder: '',
|
||||
}
|
||||
|
||||
this.downloadGame = this.downloadGame.bind(this)
|
||||
@@ -37,7 +37,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({
|
||||
dirPlaceholder: await translate('components.select_folder')
|
||||
dirPlaceholder: await translate('components.select_folder'),
|
||||
})
|
||||
|
||||
console.log(this.state)
|
||||
@@ -45,37 +45,46 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGame() {
|
||||
const folder = this.state.gameDownloadFolder
|
||||
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', () =>{
|
||||
unzip(folder + '\\game.zip', folder + '\\', () => {
|
||||
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', async () => {
|
||||
await unzip(folder + '\\game.zip', folder + '\\', true)
|
||||
this.setState({
|
||||
gameDownloading: false
|
||||
})
|
||||
gameDownloading: false,
|
||||
})
|
||||
})
|
||||
|
||||
this.setState({
|
||||
gameDownloading: true
|
||||
gameDownloading: true,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Menu heading='Download Game' closeFn={this.props.closeFn} className="GameDownloadMenu">
|
||||
<Menu heading="Download Game" closeFn={this.props.closeFn} className="GameDownloadMenu">
|
||||
<div className="GameDownload">
|
||||
{
|
||||
this.state.gameDownloadFolder !== '' && !this.state.gameDownloading ?
|
||||
<BigButton id="downloadGameBtn" onClick={this.downloadGame}>Download Game</BigButton>
|
||||
: <BigButton id="disabledGameBtn" onClick={() => null} disabled>Download Game</BigButton>
|
||||
}
|
||||
<HelpButton>
|
||||
<Tr text="main.game_help_text" />
|
||||
</HelpButton>
|
||||
{this.state.gameDownloadFolder !== '' && !this.state.gameDownloading ? (
|
||||
<BigButton id="downloadGameBtn" onClick={this.downloadGame}>
|
||||
Download Game
|
||||
</BigButton>
|
||||
) : (
|
||||
<BigButton id="disabledGameBtn" onClick={() => null} disabled>
|
||||
Download Game
|
||||
</BigButton>
|
||||
)}
|
||||
<HelpButton contents="main.game_help_text" />
|
||||
</div>
|
||||
|
||||
<div className="GameDownloadDir">
|
||||
<DirInput folder placeholder={this.state.dirPlaceholder} clearable={false} readonly={true} onChange={(value: string) => this.setState({
|
||||
gameDownloadFolder: value
|
||||
})}/>
|
||||
<DirInput
|
||||
folder
|
||||
placeholder={this.state.dirPlaceholder}
|
||||
clearable={false}
|
||||
readonly={true}
|
||||
onChange={(value: string) =>
|
||||
this.setState({
|
||||
gameDownloadFolder: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
|
||||
@@ -4,10 +4,10 @@ import './Menu.css'
|
||||
import Close from '../../../resources/icons/close.svg'
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode[] | React.ReactNode;
|
||||
className?: string;
|
||||
heading: string;
|
||||
closeFn: () => void;
|
||||
children: React.ReactNode[] | React.ReactNode
|
||||
className?: string
|
||||
heading: string
|
||||
closeFn: () => void
|
||||
}
|
||||
|
||||
export default class Menu extends React.Component<IProps, never> {
|
||||
@@ -18,13 +18,15 @@ export default class Menu extends React.Component<IProps, never> {
|
||||
render() {
|
||||
return (
|
||||
<div className={'Menu ' + this.props.className} id="menuContainer">
|
||||
<div className='MenuTop' id="menuContainerTop">
|
||||
<div className="MenuHeading" id="menuHeading">{this.props.heading}</div>
|
||||
<div className="MenuTop" id="menuContainerTop">
|
||||
<div className="MenuHeading" id="menuHeading">
|
||||
{this.props.heading}
|
||||
</div>
|
||||
<div className="MenuExit" id="menuButtonCloseContainer" onClick={this.props.closeFn}>
|
||||
<img src={Close} className="MenuClose" id="menuButtonCloseIcon" />
|
||||
</div>
|
||||
</div>
|
||||
<div className='MenuInner' id="menuContent">
|
||||
<div className="MenuInner" id="menuContent">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,3 +16,7 @@
|
||||
.OptionSection .BigButtonText {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.OptionSection .HelpButton img {
|
||||
filter: invert(0%) sepia(91%) saturate(7464%) hue-rotate(101deg) brightness(0%) contrast(107%);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ import './Options.css'
|
||||
import BigButton from '../common/BigButton'
|
||||
import DownloadHandler from '../../../utils/download'
|
||||
import * as meta from '../../../utils/metadata'
|
||||
import HelpButton from '../common/HelpButton'
|
||||
|
||||
interface IProps {
|
||||
closeFn: () => void;
|
||||
downloadManager: DownloadHandler;
|
||||
closeFn: () => void
|
||||
downloadManager: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -25,16 +26,20 @@ interface IState {
|
||||
grasscutter_path: string
|
||||
java_path: string
|
||||
grasscutter_with_game: boolean
|
||||
language_options: { [key: string]: string }[],
|
||||
language_options: { [key: string]: string }[]
|
||||
current_language: string
|
||||
bg_url_or_path: string
|
||||
themes: string[]
|
||||
theme: string
|
||||
encryption: boolean
|
||||
patch_metadata: boolean
|
||||
use_internal_proxy: boolean
|
||||
swag: boolean
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: string
|
||||
migoto_path: string
|
||||
reshade_path: string
|
||||
}
|
||||
|
||||
export default class Options extends React.Component<IProps, IState> {
|
||||
@@ -52,20 +57,27 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
themes: ['default'],
|
||||
theme: '',
|
||||
encryption: false,
|
||||
patch_metadata: false,
|
||||
use_internal_proxy: false,
|
||||
swag: false,
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: '',
|
||||
migoto_path: '',
|
||||
reshade_path: '',
|
||||
}
|
||||
|
||||
this.setGameExecutable = this.setGameExecutable.bind(this)
|
||||
this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
|
||||
this.setJavaPath = this.setJavaPath.bind(this)
|
||||
this.setAkebi = this.setAkebi.bind(this)
|
||||
this.setMigoto = this.setMigoto.bind(this)
|
||||
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
|
||||
this.setCustomBackground = this.setCustomBackground.bind(this)
|
||||
this.toggleEncryption = this.toggleEncryption.bind(this)
|
||||
this.restoreMetadata = this.restoreMetadata.bind(this)
|
||||
this.toggleMetadata = this.toggleMetadata.bind(this)
|
||||
this.toggleProxy = this.toggleProxy.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
@@ -88,10 +100,14 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
themes: (await getThemeList()).map((t) => t.name),
|
||||
theme: config.theme || 'default',
|
||||
encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled'),
|
||||
patch_metadata: config.patch_metadata || false,
|
||||
use_internal_proxy: config.use_internal_proxy || false,
|
||||
swag: config.swag_mode || false,
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: config.akebi_path || '',
|
||||
migoto_path: config.migoto_path || '',
|
||||
reshade_path: config.reshade_path || '',
|
||||
})
|
||||
|
||||
this.forceUpdate()
|
||||
@@ -125,7 +141,23 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
setConfigOption('akebi_path', value)
|
||||
|
||||
this.setState({
|
||||
akebi_path: value
|
||||
akebi_path: value,
|
||||
})
|
||||
}
|
||||
|
||||
setMigoto(value: string) {
|
||||
setConfigOption('migoto_path', value)
|
||||
|
||||
this.setState({
|
||||
migoto_path: value,
|
||||
})
|
||||
}
|
||||
|
||||
setReshade(value: string) {
|
||||
setConfigOption('reshade_path', value)
|
||||
|
||||
this.setState({
|
||||
reshade_path: value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,13 +227,32 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
async restoreMetadata() {
|
||||
console.log(this.props)
|
||||
await meta.restoreMetadata(this.props.downloadManager)
|
||||
}
|
||||
|
||||
async installCert() {
|
||||
await invoke('generate_ca_files', {
|
||||
path: await dataDir() + 'cultivation'
|
||||
path: (await dataDir()) + 'cultivation',
|
||||
})
|
||||
}
|
||||
|
||||
async toggleMetadata() {
|
||||
const changedVal = !(await getConfigOption('patch_metadata'))
|
||||
|
||||
await setConfigOption('patch_metadata', changedVal)
|
||||
|
||||
this.setState({
|
||||
patch_metadata: changedVal,
|
||||
})
|
||||
}
|
||||
|
||||
async toggleProxy() {
|
||||
const changedVal = !(await getConfigOption('use_internal_proxy'))
|
||||
|
||||
await setConfigOption('use_internal_proxy', changedVal)
|
||||
|
||||
this.setState({
|
||||
use_internal_proxy: changedVal,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -219,15 +270,37 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
<div className="OptionSection" id="menuOptionsContainermetaDownload">
|
||||
<div className="OptionLabel" id="menuOptionsLabelmetaDownload">
|
||||
<Tr text="options.recover_metadata" />
|
||||
<HelpButton contents="help.emergency_metadata" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsButtonmetaDownload">
|
||||
<BigButton onClick={this.restoreMetadata} id="metaDownload">
|
||||
<Tr text='components.download' />
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='OptionSection' id="menuOptionsContainerGCJar">
|
||||
<div className='OptionLabel' id="menuOptionsLabelGCJar">
|
||||
<div className="OptionSection" id="menuOptionsContainerPatchMeta">
|
||||
<div className="OptionLabel" id="menuOptionsLabelPatchMeta">
|
||||
<Tr text="options.patch_metadata" />
|
||||
<HelpButton contents="help.patch_metadata" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsCheckboxPatchMeta">
|
||||
<Checkbox onChange={this.toggleMetadata} checked={this.state?.patch_metadata} id="patchMeta" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="OptionSection" id="menuOptionsContainerUseProxy">
|
||||
<div className="OptionLabel" id="menuOptionsLabelUseProxy">
|
||||
<Tr text="options.use_proxy" />
|
||||
<HelpButton contents="help.use_proxy" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsCheckboxUseProxy">
|
||||
<Checkbox onChange={this.toggleProxy} checked={this.state?.use_internal_proxy} id="useProxy" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="OptionSection" id="menuOptionsContainerGCJar">
|
||||
<div className="OptionLabel" id="menuOptionsLabelGCJar">
|
||||
<Tr text="options.grasscutter_jar" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsDirGCJar">
|
||||
@@ -237,6 +310,7 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
<div className="OptionSection" id="menuOptionsContainerToggleEnc">
|
||||
<div className="OptionLabel" id="menuOptionsLabelToggleEnc">
|
||||
<Tr text="options.toggle_encryption" />
|
||||
<HelpButton contents="help.encryption" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsButtonToggleEnc">
|
||||
<BigButton onClick={this.toggleEncryption} id="toggleEnc">
|
||||
@@ -244,31 +318,45 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
</BigButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='OptionSection' id="menuOptionsContainerInstallCert">
|
||||
<div className='OptionLabel' id="menuOptionsLabelInstallCert">
|
||||
<div className="OptionSection" id="menuOptionsContainerInstallCert">
|
||||
<div className="OptionLabel" id="menuOptionsLabelInstallCert">
|
||||
<Tr text="options.install_certificate" />
|
||||
</div>
|
||||
<div className='OptionValue' id="menuOptionsButtonInstallCert">
|
||||
<div className="OptionValue" id="menuOptionsButtonInstallCert">
|
||||
<BigButton disabled={false} onClick={this.installCert} id="installCert">
|
||||
<Tr text="components.install" />
|
||||
</BigButton>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.state.swag && (
|
||||
{this.state.swag && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className='OptionSection' id="menuOptionsContainerAkebi">
|
||||
<div className='OptionLabel' id="menuOptionsLabelAkebi">
|
||||
<div className="OptionSection" id="menuOptionsContainerAkebi">
|
||||
<div className="OptionLabel" id="menuOptionsLabelAkebi">
|
||||
<Tr text="swag.akebi" />
|
||||
</div>
|
||||
<div className='OptionValue' id="menuOptionsDirAkebi">
|
||||
<div className="OptionValue" id="menuOptionsDirAkebi">
|
||||
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="OptionSection" id="menuOptionsContainerMigoto">
|
||||
<div className="OptionLabel" id="menuOptionsLabelMigoto">
|
||||
<Tr text="swag.migoto" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsDirMigoto">
|
||||
<DirInput onChange={this.setMigoto} value={this.state?.migoto_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="OptionSection" id="menuOptionsContainerReshade">
|
||||
<div className="OptionLabel" id="menuOptionsLabelReshade">
|
||||
<Tr text="swag.reshade" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsDirReshade">
|
||||
<DirInput onChange={this.setReshade} value={this.state?.reshade_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
|
||||
39
src/ui/components/mods/LoadingCircle.css
Normal file
39
src/ui/components/mods/LoadingCircle.css
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Blatantly yoinked from https://loading.io/css/
|
||||
*/
|
||||
|
||||
.LoadingCircle {
|
||||
display: inline-block;
|
||||
transform: translateZ(1px);
|
||||
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 48.5%;
|
||||
}
|
||||
|
||||
.LoadingCircle > div {
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
animation: loading 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0%,
|
||||
100% {
|
||||
animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
|
||||
}
|
||||
0% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotateY(1800deg);
|
||||
animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
|
||||
}
|
||||
100% {
|
||||
transform: rotateY(3600deg);
|
||||
}
|
||||
}
|
||||
13
src/ui/components/mods/LoadingCircle.tsx
Normal file
13
src/ui/components/mods/LoadingCircle.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
import './LoadingCircle.css'
|
||||
|
||||
export class LoadingCircle extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="LoadingCircle">
|
||||
<div></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
30
src/ui/components/mods/ModHeader.css
Normal file
30
src/ui/components/mods/ModHeader.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.ModHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(77, 77, 77, 0.6);
|
||||
}
|
||||
|
||||
.ModHeaderTitle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.ModHeaderTitle:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ModHeaderTitle.selected {
|
||||
border-bottom: 2px solid #fff;
|
||||
}
|
||||
52
src/ui/components/mods/ModHeader.tsx
Normal file
52
src/ui/components/mods/ModHeader.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
|
||||
import './ModHeader.css'
|
||||
|
||||
interface IProps {
|
||||
headers: {
|
||||
title: string
|
||||
name: string
|
||||
}[]
|
||||
onChange: (value: string) => void
|
||||
defaultHeader: string
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selected: string
|
||||
}
|
||||
|
||||
export class ModHeader extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
selected: this.props.defaultHeader,
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(value: string) {
|
||||
this.setState({
|
||||
selected: value,
|
||||
})
|
||||
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ModHeader">
|
||||
{this.props.headers.map((header, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`ModHeaderTitle ${this.state.selected === header.name ? 'selected' : ''}`}
|
||||
onClick={() => this.setSelected(header.name)}
|
||||
>
|
||||
{header.title}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
19
src/ui/components/mods/ModList.css
Normal file
19
src/ui/components/mods/ModList.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.ModList {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: rgba(106, 105, 106, 0.6);
|
||||
}
|
||||
|
||||
.ModListInner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
overflow-y: auto;
|
||||
}
|
||||
93
src/ui/components/mods/ModList.tsx
Normal file
93
src/ui/components/mods/ModList.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
import { getInstalledMods, getMods, ModData, PartialModData } from '../../../utils/gamebanana'
|
||||
import { LoadingCircle } from './LoadingCircle'
|
||||
|
||||
import './ModList.css'
|
||||
import { ModTile } from './ModTile'
|
||||
|
||||
interface IProps {
|
||||
mode: string
|
||||
addDownload: (mod: ModData) => void
|
||||
}
|
||||
|
||||
interface IState {
|
||||
modList: ModData[] | null
|
||||
installedList:
|
||||
| {
|
||||
path: string
|
||||
info: ModData | PartialModData
|
||||
}[]
|
||||
| null
|
||||
}
|
||||
|
||||
export class ModList extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
modList: null,
|
||||
installedList: null,
|
||||
}
|
||||
|
||||
this.downloadMod = this.downloadMod.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.mode === 'installed') {
|
||||
const installedMods = (await getInstalledMods()).map((mod) => {
|
||||
// Check if it's a partial mod, and if so, fill in some pseudo-data
|
||||
if (!('id' in mod.info)) {
|
||||
const newInfo = mod.info as PartialModData
|
||||
|
||||
newInfo.images = []
|
||||
newInfo.submitter = { name: 'Unknown' }
|
||||
newInfo.likes = 0
|
||||
newInfo.views = 0
|
||||
|
||||
mod.info = newInfo
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
return mod
|
||||
})
|
||||
|
||||
this.setState({
|
||||
installedList: installedMods,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const mods = await getMods(this.props.mode)
|
||||
|
||||
this.setState({
|
||||
modList: mods,
|
||||
})
|
||||
}
|
||||
|
||||
async downloadMod(mod: ModData) {
|
||||
this.props.addDownload(mod)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ModList">
|
||||
{(this.state.modList && this.props.mode !== 'installed') ||
|
||||
(this.state.installedList && this.props.mode === 'installed') ? (
|
||||
<div className="ModListInner">
|
||||
{this.props.mode === 'installed'
|
||||
? this.state.installedList?.map((mod) => (
|
||||
<ModTile path={mod.path} mod={mod.info} key={mod.info.name} onClick={this.downloadMod} />
|
||||
))
|
||||
: this.state.modList?.map((mod: ModData) => (
|
||||
<ModTile mod={mod} key={mod.id} onClick={this.downloadMod} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<LoadingCircle />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
137
src/ui/components/mods/ModTile.css
Normal file
137
src/ui/components/mods/ModTile.css
Normal file
@@ -0,0 +1,137 @@
|
||||
.ModListItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
width: 23%;
|
||||
margin: 10px;
|
||||
|
||||
background: rgb(99, 98, 98, 0.2);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.ModListItem:hover {
|
||||
cursor: pointer;
|
||||
background: rgb(99, 98, 98, 0.8);
|
||||
}
|
||||
|
||||
.ModAuthor,
|
||||
.ModName {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 10px 0 0 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ModAuthor {
|
||||
font-weight: normal;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.ModTileOpen {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ModListItem .Checkbox {
|
||||
filter: invert(100%) sepia(2%) saturate(201%) hue-rotate(47deg) brightness(117%) contrast(100%);
|
||||
}
|
||||
|
||||
.ModImage .ModTileFolder {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.ModTileFolder:hover {
|
||||
cursor: pointer;
|
||||
filter: invert(1) brightness(0.2);
|
||||
}
|
||||
|
||||
.ModTileOpen,
|
||||
.ModTileDownload {
|
||||
position: absolute;
|
||||
object-fit: contain;
|
||||
|
||||
left: 40%;
|
||||
top: 45%;
|
||||
|
||||
z-index: 999;
|
||||
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.ModTileOpen {
|
||||
left: 37.5%;
|
||||
}
|
||||
|
||||
.ModImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ModImage .ModImageInner {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
|
||||
margin: 14px;
|
||||
}
|
||||
|
||||
img.blur {
|
||||
filter: blur(6px);
|
||||
}
|
||||
|
||||
img.nsfw {
|
||||
filter: blur(16px);
|
||||
}
|
||||
|
||||
.ModInner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
color: #fff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.ModInner div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.ModInner div span {
|
||||
margin: 0px 5px;
|
||||
}
|
||||
|
||||
.ModInner img {
|
||||
object-fit: contain;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
margin: 0px;
|
||||
|
||||
filter: invert(1);
|
||||
}
|
||||
126
src/ui/components/mods/ModTile.tsx
Normal file
126
src/ui/components/mods/ModTile.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react'
|
||||
import { ModData, PartialModData } from '../../../utils/gamebanana'
|
||||
|
||||
import './ModTile.css'
|
||||
import Like from '../../../resources/icons/like.svg'
|
||||
import Eye from '../../../resources/icons/eye.svg'
|
||||
import Download from '../../../resources/icons/download.svg'
|
||||
import Folder from '../../../resources/icons/folder.svg'
|
||||
import { shell } from '@tauri-apps/api'
|
||||
import Checkbox from '../common/Checkbox'
|
||||
import { disableMod, enableMod, modIsEnabled } from '../../../utils/mods'
|
||||
|
||||
interface IProps {
|
||||
mod: ModData | PartialModData
|
||||
path?: string
|
||||
onClick: (mod: ModData) => void
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover: boolean
|
||||
modEnabled: boolean
|
||||
}
|
||||
|
||||
export class ModTile extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
modEnabled: false,
|
||||
}
|
||||
|
||||
this.openInExplorer = this.openInExplorer.bind(this)
|
||||
this.toggleMod = this.toggleMod.bind(this)
|
||||
}
|
||||
|
||||
getModFolderName() {
|
||||
if (!('id' in this.props.mod)) {
|
||||
return this.props.mod.name.includes('DISABLED_') ? this.props.mod.name.split('DISABLED_')[1] : this.props.mod.name
|
||||
}
|
||||
|
||||
return String(this.props.mod.id)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!('id' in this.props.mod)) {
|
||||
// Partial mod
|
||||
this.setState({
|
||||
modEnabled: await modIsEnabled(this.props.mod.name),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
modEnabled: await modIsEnabled(String(this.props.mod.id)),
|
||||
})
|
||||
}
|
||||
|
||||
async openInExplorer() {
|
||||
if (this.props.path) shell.open(this.props.path)
|
||||
}
|
||||
|
||||
toggleMod() {
|
||||
this.setState(
|
||||
{
|
||||
modEnabled: !this.state.modEnabled,
|
||||
},
|
||||
() => {
|
||||
if (this.state.modEnabled) {
|
||||
enableMod(String(this.getModFolderName()))
|
||||
return
|
||||
}
|
||||
|
||||
disableMod(String(this.getModFolderName()))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mod } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ModListItem"
|
||||
onMouseEnter={() => this.setState({ hover: true })}
|
||||
onMouseLeave={() => this.setState({ hover: false })}
|
||||
{...(!this.props.path && {
|
||||
onClick: () => {
|
||||
if (!('id' in mod)) return
|
||||
|
||||
this.props.onClick(mod)
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className="ModName">{mod.name.includes('DISABLED_') ? mod.name.split('DISABLED_')[1] : mod.name}</span>
|
||||
<span className="ModAuthor">{mod.submitter.name}</span>
|
||||
<div className="ModImage">
|
||||
{this.state.hover &&
|
||||
(!this.props.path ? (
|
||||
<img src={Download} className="ModTileDownload" alt="Download" />
|
||||
) : (
|
||||
<div className="ModTileOpen">
|
||||
<img src={Folder} className="ModTileFolder" alt="Open" onClick={this.openInExplorer} />
|
||||
<Checkbox checked={this.state.modEnabled} id={this.props.mod.name} onChange={this.toggleMod} />
|
||||
</div>
|
||||
))}
|
||||
<img
|
||||
src={mod.images[0]}
|
||||
className={`ModImageInner ${'id' in mod && mod.nsfw ? 'nsfw' : ''} ${this.state.hover ? 'blur' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="ModInner">
|
||||
<div className="likes">
|
||||
<img src={Like} />
|
||||
<span>{mod.likes.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="views">
|
||||
<img src={Eye} />
|
||||
<span>{mod.views.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,33 +6,33 @@ import Tr from '../../../utils/language'
|
||||
import './NewsSection.css'
|
||||
|
||||
interface IProps {
|
||||
selected?: string;
|
||||
selected?: string
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selected: string;
|
||||
news?: JSX.Element;
|
||||
commitList?: JSX.Element[];
|
||||
selected: string
|
||||
news?: JSX.Element
|
||||
commitList?: JSX.Element[]
|
||||
}
|
||||
|
||||
interface GrasscutterAPIResponse {
|
||||
commits: {
|
||||
gc_stable: CommitResponse[];
|
||||
gc_dev: CommitResponse[];
|
||||
cultivation: CommitResponse[];
|
||||
gc_stable: CommitResponse[]
|
||||
gc_dev: CommitResponse[]
|
||||
cultivation: CommitResponse[]
|
||||
}
|
||||
}
|
||||
|
||||
interface CommitResponse {
|
||||
sha: string;
|
||||
commit: Commit;
|
||||
sha: string
|
||||
commit: Commit
|
||||
}
|
||||
|
||||
interface Commit {
|
||||
author: {
|
||||
name: string;
|
||||
};
|
||||
message: string;
|
||||
name: string
|
||||
}
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class NewsSection extends React.Component<IProps, IState> {
|
||||
@@ -65,14 +65,16 @@ export default class NewsSection extends React.Component<IProps, IState> {
|
||||
|
||||
try {
|
||||
grasscutterApiResponse = JSON.parse(response)
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
grasscutterApiResponse = null
|
||||
}
|
||||
|
||||
let commits: CommitResponse[]
|
||||
if (grasscutterApiResponse?.commits == null) {
|
||||
// If it didn't work, use official API
|
||||
const response: string = await invoke('req_get', { url: 'https://api.github.com/repos/Grasscutters/Grasscutter/commits' })
|
||||
const response: string = await invoke('req_get', {
|
||||
url: 'https://api.github.com/repos/Grasscutters/Grasscutter/commits',
|
||||
})
|
||||
commits = JSON.parse(response)
|
||||
} else {
|
||||
commits = grasscutterApiResponse.commits.gc_stable
|
||||
@@ -86,15 +88,19 @@ export default class NewsSection extends React.Component<IProps, IState> {
|
||||
const commitsListHtml = commitsList.map((commitResponse: CommitResponse) => {
|
||||
return (
|
||||
<tr className="Commit" id="newsCommitsTable" key={commitResponse.sha}>
|
||||
<td className="CommitAuthor"><span>{commitResponse.commit.author.name}</span></td>
|
||||
<td className="CommitMessage"><span>{commitResponse.commit.message}</span></td>
|
||||
<td className="CommitAuthor">
|
||||
<span>{commitResponse.commit.author.name}</span>
|
||||
</td>
|
||||
<td className="CommitMessage">
|
||||
<span>{commitResponse.commit.message}</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
this.setState({
|
||||
commitList: commitsListHtml,
|
||||
news: <>{commitsListHtml}</>
|
||||
news: <>{commitsListHtml}</>,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,7 +110,7 @@ export default class NewsSection extends React.Component<IProps, IState> {
|
||||
async showNews() {
|
||||
let news: JSX.Element | JSX.Element[] = <tr></tr>
|
||||
|
||||
switch(this.state.selected) {
|
||||
switch (this.state.selected) {
|
||||
case 'commits': {
|
||||
const commits = await this.showLatestCommits()
|
||||
if (commits != null) {
|
||||
@@ -114,16 +120,24 @@ export default class NewsSection extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
case 'latest_version':
|
||||
news = <tr><td>Latest version</td></tr>
|
||||
news = (
|
||||
<tr>
|
||||
<td>Latest version</td>
|
||||
</tr>
|
||||
)
|
||||
break
|
||||
|
||||
default:
|
||||
news = <tr><td>Unknown</td></tr>
|
||||
news = (
|
||||
<tr>
|
||||
<td>Unknown</td>
|
||||
</tr>
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
this.setState({
|
||||
news: <>{news}</>
|
||||
news: <>{news}</>,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -131,17 +145,23 @@ export default class NewsSection extends React.Component<IProps, IState> {
|
||||
return (
|
||||
<div className="NewsSection" id="newsContainer">
|
||||
<div className="NewsTabs" id="newsTabsContainer">
|
||||
<div className={'NewsTab ' + (this.state.selected === 'commits' ? 'selected' : '')} id="commits" onClick={() => this.setSelected('commits')}>
|
||||
<div
|
||||
className={'NewsTab ' + (this.state.selected === 'commits' ? 'selected' : '')}
|
||||
id="commits"
|
||||
onClick={() => this.setSelected('commits')}
|
||||
>
|
||||
<Tr text="news.latest_commits" />
|
||||
</div>
|
||||
<div className={'NewsTab ' + (this.state.selected === 'latest_version' ? 'selected' : '')} id="latest_version" onClick={() => this.setSelected('latest_version')}>
|
||||
<div
|
||||
className={'NewsTab ' + (this.state.selected === 'latest_version' ? 'selected' : '')}
|
||||
id="latest_version"
|
||||
onClick={() => this.setSelected('latest_version')}
|
||||
>
|
||||
<Tr text="news.latest_version" />
|
||||
</div>
|
||||
</div>
|
||||
<table className="NewsContent" id="newsContent">
|
||||
<tbody>
|
||||
{this.state.news}
|
||||
</tbody>
|
||||
<tbody>{this.state.news}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,8 +3,7 @@ import { dataDir } from '@tauri-apps/api/path'
|
||||
|
||||
let configFilePath: string
|
||||
let defaultConfig: Configuration
|
||||
|
||||
(async() => {
|
||||
;(async () => {
|
||||
defaultConfig = {
|
||||
toggle_grasscutter: false,
|
||||
game_install_path: 'C:\\Program Files\\Genshin Impact\\Genshin Impact game\\GenshinImpact.exe',
|
||||
@@ -20,7 +19,9 @@ let defaultConfig: Configuration
|
||||
cert_generated: false,
|
||||
theme: 'default',
|
||||
https_enabled: false,
|
||||
debug_enabled: false
|
||||
debug_enabled: false,
|
||||
patch_metadata: true,
|
||||
use_internal_proxy: true,
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -43,24 +44,33 @@ export interface Configuration {
|
||||
theme: string
|
||||
https_enabled: boolean
|
||||
debug_enabled: boolean
|
||||
patch_metadata: boolean
|
||||
use_internal_proxy: boolean
|
||||
swag_mode?: boolean
|
||||
|
||||
// Swag stuff
|
||||
akebi_path?: string
|
||||
migoto_path?: string
|
||||
reshade_path?: string
|
||||
last_extras?: {
|
||||
migoto: boolean
|
||||
akebi: boolean
|
||||
reshade: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export async function setConfigOption<K extends keyof Configuration>(key: K, value: Configuration[K]): Promise<void> {
|
||||
const config = await getConfig()
|
||||
config[key] = value
|
||||
|
||||
await saveConfig(<Configuration> config)
|
||||
await saveConfig(<Configuration>config)
|
||||
}
|
||||
|
||||
export async function getConfigOption<K extends keyof Configuration>(key: K): Promise<Configuration[K]> {
|
||||
const config = await getConfig()
|
||||
const defaults = defaultConfig
|
||||
|
||||
return config[key] || defaults[key]
|
||||
return config[key] === null || config[key] === undefined ? defaults[key] : config[key]
|
||||
}
|
||||
|
||||
export async function getConfig() {
|
||||
@@ -68,8 +78,8 @@ export async function getConfig() {
|
||||
let parsed: Configuration = defaultConfig
|
||||
|
||||
try {
|
||||
parsed = <Configuration> JSON.parse(raw)
|
||||
} catch(e) {
|
||||
parsed = <Configuration>JSON.parse(raw)
|
||||
} catch (e) {
|
||||
// We could not open the file
|
||||
console.log(e)
|
||||
|
||||
@@ -95,7 +105,7 @@ async function readConfigFile() {
|
||||
|
||||
if (!dirs.find((fileOrDir) => fileOrDir?.name === 'cultivation')) {
|
||||
// Create dir
|
||||
await fs.createDir(local + 'cultivation').catch(e => console.log(e))
|
||||
await fs.createDir(local + 'cultivation').catch((e) => console.log(e))
|
||||
}
|
||||
|
||||
const innerDirs = await fs.readDir(local + '/cultivation')
|
||||
@@ -103,7 +113,7 @@ async function readConfigFile() {
|
||||
// Create grasscutter dir for potential installation
|
||||
if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) {
|
||||
// Create dir
|
||||
await fs.createDir(local + 'cultivation/grasscutter').catch(e => console.log(e))
|
||||
await fs.createDir(local + 'cultivation/grasscutter').catch((e) => console.log(e))
|
||||
}
|
||||
|
||||
const dataFiles = await fs.readDir(local + 'cultivation')
|
||||
@@ -113,7 +123,7 @@ async function readConfigFile() {
|
||||
// Create config file
|
||||
const file: fs.FsTextFileOption = {
|
||||
path: configFilePath,
|
||||
contents: JSON.stringify(defaultConfig)
|
||||
contents: JSON.stringify(defaultConfig),
|
||||
}
|
||||
|
||||
await fs.writeFile(file)
|
||||
@@ -127,6 +137,6 @@ async function writeConfigFile(raw: string) {
|
||||
// All external config functions call readConfigFile, which ensure files exists
|
||||
await fs.writeFile({
|
||||
path: configFilePath,
|
||||
contents: raw
|
||||
contents: raw,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ import { byteToString } from './string'
|
||||
|
||||
export default class DownloadHandler {
|
||||
downloads: {
|
||||
path: string,
|
||||
progress: number,
|
||||
total: number,
|
||||
total_downloaded: number,
|
||||
status: string,
|
||||
startTime: number,
|
||||
error?: string,
|
||||
speed?: string,
|
||||
onFinish?: () => void,
|
||||
path: string
|
||||
progress: number
|
||||
total: number
|
||||
total_downloaded: number
|
||||
status: string
|
||||
startTime: number
|
||||
error?: string
|
||||
speed?: string
|
||||
onFinish?: () => void
|
||||
}[]
|
||||
|
||||
// Pass tauri invoke function
|
||||
@@ -22,13 +22,13 @@ export default class DownloadHandler {
|
||||
listen('download_progress', ({ payload }) => {
|
||||
// @ts-expect-error Payload may be unknown but backend always returns this object
|
||||
const obj: {
|
||||
downloaded: string,
|
||||
total: string,
|
||||
path: string,
|
||||
total_downloaded: string,
|
||||
downloaded: string
|
||||
total: string
|
||||
path: string
|
||||
total_downloaded: string
|
||||
} = payload
|
||||
|
||||
const index = this.downloads.findIndex(download => download.path === obj.path)
|
||||
const index = this.downloads.findIndex((download) => download.path === obj.path)
|
||||
this.downloads[index].progress = parseInt(obj.downloaded, 10)
|
||||
this.downloads[index].total = parseInt(obj.total, 10)
|
||||
this.downloads[index].total_downloaded = parseInt(obj.total_downloaded, 10)
|
||||
@@ -52,7 +52,7 @@ export default class DownloadHandler {
|
||||
const filename = payload
|
||||
|
||||
// set status to finished
|
||||
const index = this.downloads.findIndex(download => download.path === filename)
|
||||
const index = this.downloads.findIndex((download) => download.path === filename)
|
||||
this.downloads[index].status = 'finished'
|
||||
|
||||
// Call onFinish callback
|
||||
@@ -65,12 +65,12 @@ export default class DownloadHandler {
|
||||
listen('download_error', ({ payload }) => {
|
||||
// @ts-expect-error shut up typescript
|
||||
const errorData: {
|
||||
path: string,
|
||||
error: string,
|
||||
path: string
|
||||
error: string
|
||||
} = payload
|
||||
|
||||
// Set download to error
|
||||
const index = this.downloads.findIndex(download => download.path === errorData.path)
|
||||
const index = this.downloads.findIndex((download) => download.path === errorData.path)
|
||||
this.downloads[index].status = 'error'
|
||||
this.downloads[index].error = errorData.error
|
||||
})
|
||||
@@ -78,13 +78,13 @@ export default class DownloadHandler {
|
||||
// Extraction events
|
||||
listen('extract_start', ({ payload }) => {
|
||||
// Find the download that is no extracting and set it's status as such
|
||||
const index = this.downloads.findIndex(download => download.path === payload)
|
||||
const index = this.downloads.findIndex((download) => download.path === payload)
|
||||
this.downloads[index].status = 'extracting'
|
||||
})
|
||||
|
||||
listen('extract_end', ({ payload }) => {
|
||||
// Find the download that is no extracting and set it's status as such
|
||||
const index = this.downloads.findIndex(download => download.path === payload)
|
||||
const index = this.downloads.findIndex((download) => download.path === payload)
|
||||
this.downloads[index].status = 'finished'
|
||||
})
|
||||
}
|
||||
@@ -95,16 +95,16 @@ export default class DownloadHandler {
|
||||
|
||||
downloadingJar() {
|
||||
// Kinda hacky but it works
|
||||
return this.downloads.some(d => d.path.includes('grasscutter.zip'))
|
||||
return this.downloads.some((d) => d.path.includes('grasscutter.zip'))
|
||||
}
|
||||
|
||||
downloadingResources() {
|
||||
// Kinda hacky but it works
|
||||
return this.downloads.some(d => d.path.includes('resources'))
|
||||
return this.downloads.some((d) => d.path.includes('resources'))
|
||||
}
|
||||
|
||||
downloadingRepo() {
|
||||
return this.downloads.some(d => d.path.includes('grasscutter_repo.zip'))
|
||||
return this.downloads.some((d) => d.path.includes('grasscutter_repo.zip'))
|
||||
}
|
||||
|
||||
addDownload(url: string, path: string, onFinish?: () => void) {
|
||||
@@ -128,24 +128,24 @@ export default class DownloadHandler {
|
||||
invoke('stop_download', { path })
|
||||
|
||||
// Remove from list
|
||||
const index = this.downloads.findIndex(download => download.path === path)
|
||||
const index = this.downloads.findIndex((download) => download.path === path)
|
||||
this.downloads.splice(index, 1)
|
||||
}
|
||||
|
||||
getDownloadProgress(path: string) {
|
||||
const index = this.downloads.findIndex(download => download.path === path)
|
||||
const index = this.downloads.findIndex((download) => download.path === path)
|
||||
return this.downloads[index] || null
|
||||
}
|
||||
|
||||
getDownloadSize(path: string) {
|
||||
const index = this.downloads.findIndex(download => download.path === path)
|
||||
const index = this.downloads.findIndex((download) => download.path === path)
|
||||
return byteToString(this.downloads[index].total) || null
|
||||
}
|
||||
|
||||
getTotalAverage() {
|
||||
const files = this.downloads.filter(d => d.status === 'downloading')
|
||||
const files = this.downloads.filter((d) => d.status === 'downloading')
|
||||
const total = files.reduce((acc, d) => acc + d.total, 0)
|
||||
const progress = files.reduce((acc, d) => d.progress !== 0 ? acc + d.progress : acc + d.total_downloaded, 0)
|
||||
const progress = files.reduce((acc, d) => (d.progress !== 0 ? acc + d.progress : acc + d.total_downloaded), 0)
|
||||
let speedStr = '0 B/s'
|
||||
|
||||
// Get download speed based on startTimes
|
||||
@@ -158,10 +158,10 @@ export default class DownloadHandler {
|
||||
|
||||
return {
|
||||
average: (progress / total) * 100 || 0,
|
||||
files: this.downloads.filter(d => d.status === 'downloading').length,
|
||||
extracting: this.downloads.filter(d => d.status === 'extracting').length,
|
||||
files: this.downloads.filter((d) => d.status === 'downloading').length,
|
||||
extracting: this.downloads.filter((d) => d.status === 'extracting').length,
|
||||
totalSize: total,
|
||||
speed: speedStr
|
||||
speed: speedStr,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { getConfig } from './configuration'
|
||||
export async function getGameExecutable() {
|
||||
const config = await getConfig()
|
||||
|
||||
if(!config.game_install_path) {
|
||||
if (!config.game_install_path) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function getGameExecutable() {
|
||||
export async function getGameFolder() {
|
||||
const config = await getConfig()
|
||||
|
||||
if(!config.game_install_path) {
|
||||
if (!config.game_install_path) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
205
src/utils/gamebanana.ts
Normal file
205
src/utils/gamebanana.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { getConfigOption } from './configuration'
|
||||
|
||||
// Generated with https://transform.tools/json-to-typescript I'm lazy cry about it
|
||||
interface GamebananaResponse {
|
||||
_idRow: number
|
||||
_sModelName: string
|
||||
_sSingularTitle: string
|
||||
_sIconClasses: string
|
||||
_sName: string
|
||||
_sProfileUrl: string
|
||||
_aPreviewMedia: PreviewMedia
|
||||
_tsDateAdded: number
|
||||
_bHasFiles: boolean
|
||||
_aSubmitter: Submitter
|
||||
_aRootCategory: RootCategory
|
||||
_bIsNsfw: boolean
|
||||
_sInitialVisibility: string
|
||||
_nLikeCount?: number
|
||||
_bIsOwnedByAccessor: boolean
|
||||
_nViewCount?: number
|
||||
_nPostCount?: number
|
||||
_tsDateUpdated?: number
|
||||
}
|
||||
|
||||
interface PreviewMedia {
|
||||
_aImages?: Image[]
|
||||
_aMetadata?: Metadata
|
||||
}
|
||||
|
||||
interface Image {
|
||||
_sType: string
|
||||
_sBaseUrl: string
|
||||
_sFile: string
|
||||
_sFile530?: string
|
||||
_sFile100: string
|
||||
_sFile220?: string
|
||||
_sCaption?: string
|
||||
}
|
||||
|
||||
interface Metadata {
|
||||
_sState?: string
|
||||
_sSnippet: string
|
||||
_nPostCount?: number
|
||||
_nBounty?: number
|
||||
}
|
||||
|
||||
interface Submitter {
|
||||
_idRow: number
|
||||
_sName: string
|
||||
_bIsOnline: boolean
|
||||
_bHasRipe: boolean
|
||||
_sProfileUrl: string
|
||||
_sAvatarUrl: string
|
||||
_sUpicUrl?: string
|
||||
_aClearanceLevels?: string[]
|
||||
_sHdAvatarUrl?: string
|
||||
}
|
||||
|
||||
interface RootCategory {
|
||||
_sName: string
|
||||
_sProfileUrl: string
|
||||
_sIconUrl: string
|
||||
}
|
||||
|
||||
export interface ModData {
|
||||
id: number
|
||||
name: string
|
||||
images: string[]
|
||||
dateadded: number
|
||||
submitter: {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
nsfw: boolean
|
||||
likes: number
|
||||
views: number
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface PartialModData {
|
||||
name: string
|
||||
images: string[]
|
||||
submitter: {
|
||||
name: string
|
||||
}
|
||||
likes: number
|
||||
views: number
|
||||
}
|
||||
|
||||
interface GamebananaDownloads {
|
||||
_bIsTrashed: boolean
|
||||
_bIsWithheld: boolean
|
||||
_aFiles: File[]
|
||||
_sLicense: string
|
||||
}
|
||||
|
||||
interface File {
|
||||
_idRow: number
|
||||
_sFile: string
|
||||
_nFilesize: number
|
||||
_sDescription: string
|
||||
_tsDateAdded: number
|
||||
_nDownloadCount: number
|
||||
_sAnalysisState: string
|
||||
_sDownloadUrl: string
|
||||
_sMd5Checksum: string
|
||||
_sClamAvResult: string
|
||||
_sAnalysisResult: string
|
||||
_bContainsExe: boolean
|
||||
}
|
||||
|
||||
interface ModDownload {
|
||||
filename: string
|
||||
downloadUrl: string
|
||||
filesize: number
|
||||
containsExe: boolean
|
||||
}
|
||||
|
||||
export async function getMods(mode: string) {
|
||||
const resp = JSON.parse(
|
||||
await invoke('list_submissions', {
|
||||
mode,
|
||||
})
|
||||
)
|
||||
|
||||
return formatGamebananaData(resp)
|
||||
}
|
||||
|
||||
export async function formatGamebananaData(obj: GamebananaResponse[]) {
|
||||
if (!obj) return []
|
||||
|
||||
return obj
|
||||
.map((itm) => {
|
||||
const img = itm?._aPreviewMedia?._aImages
|
||||
|
||||
return {
|
||||
id: itm._idRow,
|
||||
name: itm._sName,
|
||||
images: img
|
||||
? img.map((i) => {
|
||||
return i._sBaseUrl + '/' + i._sFile220
|
||||
})
|
||||
: [],
|
||||
dateadded: itm._tsDateAdded,
|
||||
submitter: {
|
||||
name: itm._aSubmitter._sName,
|
||||
url: itm._aSubmitter._sProfileUrl,
|
||||
},
|
||||
nsfw: itm._bIsNsfw,
|
||||
likes: itm?._nLikeCount || 0,
|
||||
views: itm?._nViewCount || 0,
|
||||
type: itm._sSingularTitle,
|
||||
} as ModData
|
||||
})
|
||||
.filter((itm) => itm.type === 'Mod')
|
||||
}
|
||||
|
||||
export async function getInstalledMods() {
|
||||
const migotoPath = await getConfigOption('migoto_path')
|
||||
|
||||
if (!migotoPath) return []
|
||||
|
||||
const mods = (await invoke('list_mods', {
|
||||
path: migotoPath,
|
||||
})) as Record<string, string>
|
||||
|
||||
// These are returned as JSON strings, so we have to parse them
|
||||
return Object.keys(mods).map((path) => {
|
||||
const info = JSON.parse(mods[path]) as ModData | PartialModData
|
||||
|
||||
const modPathArr = path.replace(/\\/g, '/').split('/')
|
||||
|
||||
// If there is a file in this path, remove it from the path
|
||||
if (modPathArr[modPathArr.length - 1].includes('.')) modPathArr.pop()
|
||||
|
||||
return {
|
||||
path: modPathArr.join('/'),
|
||||
info,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getModDownload(modId: string) {
|
||||
const resp = JSON.parse(
|
||||
await invoke('get_download_links', {
|
||||
modId,
|
||||
})
|
||||
) as GamebananaDownloads
|
||||
|
||||
return formatDownloadsData(resp)
|
||||
}
|
||||
|
||||
export async function formatDownloadsData(obj: GamebananaDownloads) {
|
||||
if (!obj) return []
|
||||
|
||||
return obj._aFiles.map((itm) => {
|
||||
return {
|
||||
filename: itm._sFile,
|
||||
downloadUrl: `https://files.gamebanana.com/mods/${itm._sFile}`,
|
||||
filesize: itm._nFilesize,
|
||||
containsExe: itm._bContainsExe,
|
||||
} as ModDownload
|
||||
})
|
||||
}
|
||||
@@ -3,12 +3,12 @@ import React from 'react'
|
||||
import { getConfigOption } from './configuration'
|
||||
|
||||
interface IProps {
|
||||
text: string;
|
||||
text: string
|
||||
}
|
||||
|
||||
interface IState {
|
||||
language: string;
|
||||
translated_text: string;
|
||||
language: string
|
||||
translated_text: string
|
||||
}
|
||||
|
||||
export default class Tr extends React.Component<IProps, IState> {
|
||||
@@ -21,14 +21,18 @@ export default class Tr extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
const { text } = this.props
|
||||
getConfigOption('language').then((language: string) => {
|
||||
let language = await getConfigOption('language')
|
||||
|
||||
// Get translation file
|
||||
if (!language) language = 'en'
|
||||
|
||||
invoke('get_lang', { lang: language }).then((response) => {
|
||||
const translation_obj = JSON.parse(response as string || '{}')
|
||||
const response = await invoke('get_lang', { lang: language })
|
||||
const default_resp = await invoke('get_lang', { lang: 'en' })
|
||||
|
||||
const translation_obj = JSON.parse((response as string) || '{}')
|
||||
const default_obj = JSON.parse((default_resp as string) || '{}')
|
||||
|
||||
// Traversal
|
||||
if (text.includes('.')) {
|
||||
@@ -39,7 +43,20 @@ export default class Tr extends React.Component<IProps, IState> {
|
||||
if (!translation) {
|
||||
translation = ''
|
||||
} else {
|
||||
translation = typeof translation !== 'string' ? translation[keys[i]] : translation as string
|
||||
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
|
||||
}
|
||||
}
|
||||
|
||||
// If we could not find a translation, use the default one
|
||||
if (!translation) {
|
||||
translation = default_obj
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (!translation) {
|
||||
translation = ''
|
||||
} else {
|
||||
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +65,9 @@ export default class Tr extends React.Component<IProps, IState> {
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
translated_text: translation_obj[text] || ''
|
||||
translated_text: translation_obj[text] || '',
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -62,13 +77,13 @@ export default class Tr extends React.Component<IProps, IState> {
|
||||
|
||||
export async function getLanguages() {
|
||||
const resp: {
|
||||
[key: string]: string;
|
||||
[key: string]: string
|
||||
} = await invoke('get_languages')
|
||||
const lang_list: {
|
||||
[key: string]: string;
|
||||
[key: string]: string
|
||||
}[] = []
|
||||
|
||||
Object.keys(resp).forEach(k => {
|
||||
Object.keys(resp).forEach((k) => {
|
||||
const parsed = JSON.parse(resp[k])
|
||||
|
||||
if (parsed.lang_name) {
|
||||
@@ -80,8 +95,9 @@ export async function getLanguages() {
|
||||
}
|
||||
|
||||
export async function translate(text: string) {
|
||||
const language = await getConfigOption('language') || 'en'
|
||||
const translation_json = JSON.parse(await invoke('get_lang', { lang: language }) || '{}')
|
||||
const language = (await getConfigOption('language')) || 'en'
|
||||
const translation_json = JSON.parse((await invoke('get_lang', { lang: language })) || '{}')
|
||||
const default_json = JSON.parse(await invoke('get_lang', { lang: 'en' }))
|
||||
|
||||
// Traversal
|
||||
if (text.includes('.')) {
|
||||
@@ -92,7 +108,20 @@ export async function translate(text: string) {
|
||||
if (!translation) {
|
||||
translation = ''
|
||||
} else {
|
||||
translation = typeof translation !== 'string' ? translation[keys[i]] : translation as string
|
||||
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
|
||||
}
|
||||
}
|
||||
|
||||
// If we could not find a translation, use the default one
|
||||
if (!translation) {
|
||||
translation = default_json
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (!translation) {
|
||||
translation = ''
|
||||
} else {
|
||||
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getGameExecutable, getGameFolder } from './game'
|
||||
|
||||
export async function patchMetadata() {
|
||||
const metadataExists = await invoke('dir_exists', {
|
||||
path: await getGameMetadataPath() + '\\global-metadata.dat'
|
||||
path: (await getGameMetadataPath()) + '\\global-metadata.dat',
|
||||
})
|
||||
|
||||
if (!metadataExists) {
|
||||
@@ -16,9 +16,9 @@ export async function patchMetadata() {
|
||||
|
||||
// Copy unpatched metadata to backup location
|
||||
const copiedMeta = await invoke('copy_file_with_new_name', {
|
||||
path: await getGameMetadataPath() + '\\global-metadata.dat',
|
||||
path: (await getGameMetadataPath()) + '\\global-metadata.dat',
|
||||
newPath: await getBackupMetadataPath(),
|
||||
newName: 'global-metadata-unpatched.dat'
|
||||
newName: 'global-metadata-unpatched.dat',
|
||||
})
|
||||
|
||||
if (!copiedMeta) {
|
||||
@@ -42,9 +42,9 @@ export async function patchMetadata() {
|
||||
console.log('Replacing unpatched game metadata with patched metadata')
|
||||
|
||||
const replacedMeta = await invoke('copy_file_with_new_name', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-patched.dat',
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
|
||||
newPath: await getGameMetadataPath(),
|
||||
newName: 'global-metadata.dat'
|
||||
newName: 'global-metadata.dat',
|
||||
})
|
||||
|
||||
if (!replacedMeta) {
|
||||
@@ -58,7 +58,7 @@ export async function patchMetadata() {
|
||||
|
||||
export async function patchGame() {
|
||||
const backupExists = await invoke('dir_exists', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat'
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
|
||||
})
|
||||
|
||||
if (!backupExists) {
|
||||
@@ -70,10 +70,24 @@ export async function patchGame() {
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have a patch already?
|
||||
const patchedExists = await invoke('dir_exists', {
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
|
||||
})
|
||||
|
||||
if (!patchedExists) {
|
||||
// No patch found? Patching creates one
|
||||
const patched = await patchMetadata()
|
||||
|
||||
if (!patched) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Are we already patched? If so, that's fine, just continue as normal
|
||||
const gameIsPatched = await invoke('are_files_identical', {
|
||||
path1: await getBackupMetadataPath() + '\\global-metadata-patched.dat',
|
||||
path2: await getGameMetadataPath() + '\\global-metadata.dat'
|
||||
path1: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
|
||||
path2: (await getGameMetadataPath()) + '\\global-metadata.dat',
|
||||
})
|
||||
|
||||
if (gameIsPatched) {
|
||||
@@ -82,17 +96,17 @@ export async function patchGame() {
|
||||
|
||||
// Is the current backup the same as the games current metadata?
|
||||
const backupIsCurrent = await invoke('are_files_identical', {
|
||||
path1: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat',
|
||||
path2: await getGameMetadataPath() + '\\global-metadata.dat'
|
||||
path1: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
|
||||
path2: (await getGameMetadataPath()) + '\\global-metadata.dat',
|
||||
})
|
||||
|
||||
// Game has probably been updated. We need to repatch the game...
|
||||
if (!backupIsCurrent) {
|
||||
const deletedOldBackup = await invoke('delete_file', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat'
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
|
||||
})
|
||||
const deletedOldPatched = await invoke('delete_file', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-patched.dat'
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
|
||||
})
|
||||
|
||||
// It's fine if these deletes fail. The game will be replaced anyway.
|
||||
@@ -120,9 +134,9 @@ export async function patchGame() {
|
||||
|
||||
// Finally, replace the unpatched metadata with the patched one
|
||||
const replaced = await invoke('copy_file_with_new_name', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-patched.dat',
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
|
||||
newPath: await getGameMetadataPath(),
|
||||
newName: 'global-metadata.dat'
|
||||
newName: 'global-metadata.dat',
|
||||
})
|
||||
|
||||
if (!replaced) {
|
||||
@@ -134,7 +148,7 @@ export async function patchGame() {
|
||||
|
||||
export async function unpatchGame() {
|
||||
const backupExists = await invoke('dir_exists', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat'
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
|
||||
})
|
||||
|
||||
if (!backupExists) {
|
||||
@@ -142,26 +156,10 @@ export async function unpatchGame() {
|
||||
return true
|
||||
}
|
||||
|
||||
const metaPatched = await invoke('are_files_identical', {
|
||||
path1: await getBackupMetadataPath() + '\\global-metadata-patched.dat',
|
||||
path2: await getGameMetadataPath() + '\\global-metadata.dat'
|
||||
})
|
||||
|
||||
const metaExists = await invoke('dir_exists', {
|
||||
path: await getGameMetadataPath() + '\\global-metadata.dat'
|
||||
})
|
||||
|
||||
if (!metaPatched && metaExists) {
|
||||
// Game isn't patched
|
||||
return true
|
||||
}
|
||||
|
||||
console.log('Replacing patched game metadata with unpatched metadata')
|
||||
|
||||
const replaced = await invoke('copy_file_with_new_name', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat',
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
|
||||
newPath: await getGameMetadataPath(),
|
||||
newName: 'global-metadata.dat'
|
||||
newName: 'global-metadata.dat',
|
||||
})
|
||||
|
||||
return replaced
|
||||
@@ -174,20 +172,26 @@ export async function getGameMetadataPath() {
|
||||
return null
|
||||
}
|
||||
|
||||
return (await getGameFolder() + '\\' + gameExec.replace('.exe', '_Data') + '\\Managed\\Metadata').replace(/\\/g, '/')
|
||||
return ((await getGameFolder()) + '\\' + gameExec.replace('.exe', '_Data') + '\\Managed\\Metadata').replace(
|
||||
/\\/g,
|
||||
'/'
|
||||
)
|
||||
}
|
||||
|
||||
export async function getBackupMetadataPath() {
|
||||
return await dataDir() + 'cultivation\\metadata'
|
||||
return (await dataDir()) + 'cultivation\\metadata'
|
||||
}
|
||||
|
||||
export async function globalMetadataLink() {
|
||||
const versionAPIUrl = 'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10&sub_channel_id=0'
|
||||
const versionAPIUrl =
|
||||
'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10&sub_channel_id=0'
|
||||
|
||||
// Get versions from API
|
||||
const versions = JSON.parse(await invoke('web_get', {
|
||||
url: versionAPIUrl
|
||||
}))
|
||||
const versions = JSON.parse(
|
||||
await invoke('web_get', {
|
||||
url: versionAPIUrl,
|
||||
})
|
||||
)
|
||||
|
||||
if (!versions || versions.retcode !== 0) {
|
||||
console.log('Failed to get versions from API')
|
||||
@@ -197,30 +201,31 @@ export async function globalMetadataLink() {
|
||||
// Get latest version
|
||||
const latest = versions.data.game.latest
|
||||
|
||||
return latest.decompressed_path as string + '/GenshinImpact_Data/Managed/Metadata/global-metadata.dat'
|
||||
return (latest.decompressed_path as string) + '/GenshinImpact_Data/Managed/Metadata/global-metadata.dat'
|
||||
}
|
||||
|
||||
export async function restoreMetadata(manager: DownloadHandler) {
|
||||
const backupExists = await invoke('dir_exists', {
|
||||
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat'
|
||||
})
|
||||
|
||||
if (!backupExists) {
|
||||
console.log('No backup found! Replacing with global metadata link')
|
||||
|
||||
const metaLink = await globalMetadataLink()
|
||||
|
||||
if (!metaLink) {
|
||||
console.log('Coudl not get global metadata link!')
|
||||
console.log('Could not get global metadata link!')
|
||||
return false
|
||||
}
|
||||
|
||||
// Should make sure metadata path exists since the user may have deleted it
|
||||
await invoke('dir_create', {
|
||||
path: await getBackupMetadataPath(),
|
||||
})
|
||||
|
||||
// It is possible the unpatched backup is mistakenly patched
|
||||
await invoke('delete_file', {
|
||||
path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
|
||||
})
|
||||
|
||||
// Download the file
|
||||
manager.addDownload(metaLink, await getBackupMetadataPath() + '\\global-metadata-unpatched.dat', () => {
|
||||
manager.addDownload(metaLink, (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat', () => {
|
||||
unpatchGame()
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Restoring backedup metadata')
|
||||
|
||||
await unpatchGame()
|
||||
|
||||
71
src/utils/mods.ts
Normal file
71
src/utils/mods.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { getConfigOption } from './configuration'
|
||||
|
||||
export async function getModsFolder() {
|
||||
const migotoPath = await getConfigOption('migoto_path')
|
||||
|
||||
if (!migotoPath) return null
|
||||
|
||||
// Remove exe from path
|
||||
const pathArr = migotoPath.replace(/\\/g, '/').split('/')
|
||||
pathArr.pop()
|
||||
|
||||
return pathArr.join('/') + '/Mods/'
|
||||
}
|
||||
|
||||
export async function disableMod(modId: string) {
|
||||
const path = (await getModsFolder()) + modId
|
||||
const pathExists = await invoke('dir_exists', {
|
||||
path,
|
||||
})
|
||||
|
||||
if (!pathExists) return console.log("Path doesn't exist")
|
||||
|
||||
const modName = path.replace(/\\/g, '/').split('/').pop()
|
||||
|
||||
await invoke('rename', {
|
||||
path,
|
||||
newName: `DISABLED_${modName}`,
|
||||
})
|
||||
}
|
||||
|
||||
export async function enableMod(modId: string) {
|
||||
const path = (await getModsFolder()) + `DISABLED_${modId}`
|
||||
const modName = path.replace(/\\/g, '/').split('/').pop()
|
||||
const pathExists = await invoke('dir_exists', {
|
||||
path,
|
||||
})
|
||||
|
||||
if (!pathExists) return console.log("Path doesn't exist")
|
||||
|
||||
if (!modName?.includes('DISABLED_')) return
|
||||
|
||||
const newName = modName.replace('DISABLED_', '')
|
||||
|
||||
await invoke('rename', {
|
||||
path,
|
||||
newName,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getModFolderName(modId: string) {
|
||||
const modsFolder = await getModsFolder()
|
||||
|
||||
if (!modsFolder) return null
|
||||
|
||||
const modEnabled = await invoke('dir_exists', {
|
||||
path: modsFolder + modId,
|
||||
})
|
||||
const modDisabled = await invoke('dir_exists', {
|
||||
path: modsFolder + 'DISABLED_' + modId,
|
||||
})
|
||||
|
||||
if (!modEnabled && !modDisabled) return null
|
||||
|
||||
if (modEnabled) return modId
|
||||
if (modDisabled) return 'DISABLED_' + modId
|
||||
}
|
||||
|
||||
export async function modIsEnabled(modId: string) {
|
||||
return !(await getModFolderName(modId))?.includes('DISABLED_')
|
||||
}
|
||||
@@ -4,10 +4,12 @@ export async function toggleEncryption(path: string) {
|
||||
let serverConf
|
||||
|
||||
try {
|
||||
serverConf = JSON.parse(await invoke('read_file', {
|
||||
serverConf = JSON.parse(
|
||||
await invoke('read_file', {
|
||||
path,
|
||||
}))
|
||||
} catch(e) {
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(`Server config at ${path} not found or invalid`)
|
||||
return
|
||||
}
|
||||
@@ -28,10 +30,12 @@ export async function encryptionEnabled(path: string) {
|
||||
let serverConf
|
||||
|
||||
try {
|
||||
serverConf = JSON.parse(await invoke('read_file', {
|
||||
serverConf = JSON.parse(
|
||||
await invoke('read_file', {
|
||||
path,
|
||||
}))
|
||||
} catch(e) {
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(`Server config at ${path} not found or invalid`)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -33,15 +33,15 @@ const defaultTheme = {
|
||||
description: 'Default theme',
|
||||
includes: {
|
||||
css: [],
|
||||
js: []
|
||||
js: [],
|
||||
},
|
||||
path: 'default'
|
||||
path: 'default',
|
||||
}
|
||||
export async function getThemeList() {
|
||||
// Do some invoke to backend to get the theme list
|
||||
const themes = await invoke('get_theme_list', {
|
||||
dataDir: `${await dataDir()}/cultivation`
|
||||
}) as BackendThemeList[]
|
||||
const themes = (await invoke('get_theme_list', {
|
||||
dataDir: `${await dataDir()}/cultivation`,
|
||||
})) as BackendThemeList[]
|
||||
const list: ThemeList[] = [
|
||||
// ALWAYS include default theme
|
||||
{
|
||||
@@ -50,13 +50,13 @@ export async function getThemeList() {
|
||||
description: 'Default theme',
|
||||
includes: {
|
||||
css: [],
|
||||
js: []
|
||||
js: [],
|
||||
},
|
||||
path: 'default',
|
||||
},
|
||||
path: 'default'
|
||||
}
|
||||
]
|
||||
|
||||
themes.forEach(t => {
|
||||
themes.forEach((t) => {
|
||||
let obj
|
||||
|
||||
try {
|
||||
@@ -74,7 +74,7 @@ export async function getThemeList() {
|
||||
export async function getTheme(name: string) {
|
||||
const themes = await getThemeList()
|
||||
|
||||
return themes.find(t => t.name === name) || defaultTheme
|
||||
return themes.find((t) => t.name === name) || defaultTheme
|
||||
}
|
||||
|
||||
export async function loadTheme(theme: ThemeList, document: Document) {
|
||||
@@ -89,7 +89,7 @@ export async function loadTheme(theme: ThemeList, document: Document) {
|
||||
const jsIncludes = theme.includes.js
|
||||
|
||||
// Load CSS files
|
||||
cssIncludes.forEach(css => {
|
||||
cssIncludes.forEach((css) => {
|
||||
if (!css) return
|
||||
|
||||
const link = document.createElement('link')
|
||||
@@ -100,7 +100,7 @@ export async function loadTheme(theme: ThemeList, document: Document) {
|
||||
})
|
||||
|
||||
// Load JS files
|
||||
jsIncludes.forEach(js => {
|
||||
jsIncludes.forEach((js) => {
|
||||
if (!js) return
|
||||
|
||||
const script = document.createElement('script')
|
||||
@@ -125,7 +125,7 @@ export async function loadTheme(theme: ThemeList, document: Document) {
|
||||
// Save the background to our data dir
|
||||
await invoke('copy_file', {
|
||||
path: theme.path + '/' + theme.customBackgroundPath,
|
||||
newPath: bgPath
|
||||
newPath: bgPath,
|
||||
})
|
||||
|
||||
// Set the background
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
export function unzip(file: string, dest: string, onFinish?: () => void) {
|
||||
interface UnzipPayload {
|
||||
file: string
|
||||
new_folder: string
|
||||
}
|
||||
|
||||
export function unzip(
|
||||
file: string,
|
||||
dest: string,
|
||||
topLevelStrip?: boolean,
|
||||
folderIfLoose?: boolean
|
||||
): Promise<UnzipPayload> {
|
||||
return new Promise((resolve) => {
|
||||
invoke('unzip', {
|
||||
zipfile: file,
|
||||
destpath: dest,
|
||||
topLevelStrip,
|
||||
folderIfLoose,
|
||||
})
|
||||
|
||||
listen('extract_end', ({payload}) => {
|
||||
if (payload === file && onFinish) {
|
||||
onFinish()
|
||||
listen('extract_end', ({ payload }) => {
|
||||
// @ts-expect-error Payload is an object
|
||||
if (payload?.file === file) {
|
||||
resolve(payload as UnzipPayload)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -20,7 +16,5 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user