Compare commits

..

101 Commits

Author SHA1 Message Date
SpikeHD
01fce477ef Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-08-23 23:52:21 -07:00
SpikeHD
2f610ce0af version bump 2022-08-23 23:52:11 -07:00
SpikeHD
d9772d9ffb resource link update 2022-08-23 23:47:52 -07:00
SpikeHD
93097db5cc Merge pull request #58 from Untitled/main
Chinese Simplified Translations Update
2022-08-04 10:55:53 -07:00
SpikeHD
c06aa9f885 Merge pull request #57 from Kimi898246/patch-3
Traditional Chinese | Translation Patches
2022-08-04 10:55:21 -07:00
ChapterII
f7bcad0a90 Update chs.json
Updated translations
2022-08-04 15:04:27 +08:00
Kimi
7120f846f7 Traditional Chinese | Translation Patches
hi
2022-08-04 02:18:21 +08:00
SpikeHD
f6ed37d2e4 Merge pull request #53 from DasIschBims/main
Update de.json
2022-08-02 17:21:54 -07:00
DasIschBims
99b210b3be Update de.json 2022-07-30 13:49:13 +02:00
SpikeHD
7d52766c07 Merge pull request #51 from TotalyNotOndre/patch-1
Update ru.json
2022-07-28 12:18:13 -07:00
Abdul
212d974ac6 Fixed missing separators 2022-07-28 20:23:55 +03:00
SpikeHD
a6e06e3005 revert to 50 per page 2022-07-27 21:52:26 -07:00
Abdul
56c1f2dcc2 Update ru.json
- Added and translated missing localization strings
2022-07-28 03:33:22 +03:00
SpikeHD
a843888cb8 cleanup 2022-07-27 17:01:55 -07:00
SpikeHD
281bfb5cea cheeky key path fix 2022-07-26 22:30:25 -07:00
SpikeHD
79891238b6 fix reshade 2022-07-26 20:59:33 -07:00
SpikeHD
0971f5b826 progress bar 'fix' 2022-07-26 20:49:20 -07:00
SpikeHD
7e5f3be4fa cleanup 2022-07-26 20:41:55 -07:00
SpikeHD
e29e269c4c Merge branch 'mod_management' 2022-07-26 20:39:54 -07:00
SpikeHD
8f8e37aff3 close menu after launch 2022-07-26 20:39:02 -07:00
SpikeHD
6257a2e68c non-blocking commands for reshade 2022-07-26 20:37:39 -07:00
SpikeHD
203bd40e8f reshade setting and enabling 2022-07-26 20:16:20 -07:00
SpikeHD
c64cdababa save options 2022-07-26 20:01:10 -07:00
SpikeHD
1ecd38ee9f extras menu for conditional extras 2022-07-26 19:58:16 -07:00
SpikeHD
26292984a2 download selection 2022-07-25 22:46:43 -07:00
SpikeHD
cffbcdae96 handle rar files, add back button svg 2022-07-25 22:09:24 -07:00
SpikeHD
2026e2f896 fix styling 2022-07-25 20:29:52 -07:00
SpikeHD
9566beaf29 mod enabling and disabling 2022-07-25 20:25:43 -07:00
SpikeHD
c99080168c fixes 2022-07-25 19:30:13 -07:00
SpikeHD
9fa3351747 visuals for enabling/disableing 2022-07-25 19:23:05 -07:00
SpikeHD
75f1eef587 help text for some options 2022-07-25 18:45:48 -07:00
SpikeHD
43a6348b7a cleanup 2022-07-25 18:28:06 -07:00
SpikeHD
cc74107dfe move help buttons to alerts 2022-07-25 18:26:04 -07:00
SpikeHD
afa40f437f english fallback when using other languages 2022-07-25 18:13:37 -07:00
SpikeHD
95282a3f36 remove unused 2022-07-24 21:06:31 -07:00
SpikeHD
0331bb5faf log cleanup 2022-07-24 20:59:45 -07:00
SpikeHD
0551f3e6a0 solution for loose file mods 2022-07-24 20:59:08 -07:00
SpikeHD
412acdd317 remoe download list logging 2022-07-24 20:13:08 -07:00
SpikeHD
c42c708db5 fix opening and some other stuff 2022-07-24 20:12:30 -07:00
SpikeHD
552d612e7c write modinfo.json after installing mod 2022-07-24 20:03:16 -07:00
SpikeHD
5077c19fdc show installed mods 2022-07-24 19:28:05 -07:00
SpikeHD
36c2302f1b downloading and extracting 2022-07-24 18:14:18 -07:00
SpikeHD
a06a8af7df Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-07-24 16:43:59 -07:00
SpikeHD
975b04fd0e fix download icon lineup 2022-07-24 00:30:00 -07:00
SpikeHD
811f437238 potentially gather installed mods 2022-07-24 00:24:10 -07:00
SpikeHD
a25645ef77 fix category setting 2022-07-23 23:44:04 -07:00
SpikeHD
ac8fd3af45 fixes 2022-07-23 23:23:48 -07:00
SpikeHD
4f3952aeb1 downloading (almost) and only show when migoto is set 2022-07-23 23:23:48 -07:00
SpikeHD
c0740417e3 better styling, download overlay on hover 2022-07-23 23:23:48 -07:00
SpikeHD
8700a77ba0 more styling 2022-07-23 23:23:48 -07:00
SpikeHD
95267720a4 mod tiles and nsfw blurring 2022-07-23 23:23:48 -07:00
SpikeHD
d97e5c192f interepret API response 2022-07-23 23:23:48 -07:00
SpikeHD
a9d9d361e1 proper heading binding 2022-07-23 23:23:48 -07:00
SpikeHD
b78d9c28c9 loading anim 2022-07-23 23:23:48 -07:00
SpikeHD
f946cedb4d mod list colors and such 2022-07-23 23:23:48 -07:00
SpikeHD
c659979851 remove thread stuff, set cwd properly when openening migoto 2022-07-23 23:23:47 -07:00
SpikeHD
940943b106 fix migoto launch btn 2022-07-23 23:23:47 -07:00
SpikeHD
7ad1c4649c page switching with custom event 2022-07-23 23:23:47 -07:00
SpikeHD
011b15c8d9 split main launcher into seperate file 2022-07-23 23:23:47 -07:00
SpikeHD
d28af907ec better button management 2022-07-23 23:23:47 -07:00
SpikeHD
1b076ccea9 open with cwd, restore akebi option stuff 2022-07-23 23:23:47 -07:00
SpikeHD
818896c734 Merge pull request #42 from Seeker14491/cpp-fix
Don't throw C++ exceptions into Rust
2022-07-23 17:12:52 -07:00
SpikeHD
3b6225d5f0 Merge pull request #43 from trollerr/patch-1
Update Vietnamese translation
2022-07-23 15:10:27 -07:00
Trollerr
041a6cb768 typo 2022-07-23 22:03:44 +07:00
Trollerr
acb6de85ad Update Vietnamese translation 2022-07-23 22:00:31 +07:00
SpikeHD
cd628b4f3d open with cwd, restore akebi option stuff 2022-07-21 19:49:51 -07:00
SpikeHD
19d939a074 remove migoto for now 2022-07-21 19:41:00 -07:00
Brian Bowman
aeaa7ef76c Don't throw C++ exceptions into Rust 2022-07-21 18:03:43 -05:00
Brian Bowman
7659e9831a Remove unused metadata-related things 2022-07-21 17:43:30 -05:00
SpikeHD
22a416ebd8 Fix elementIds.md 2022-07-21 09:55:44 -07:00
SpikeHD
dcc9749967 Merge pull request #40 from Seeker14491/prettier
Add prettier formatter
2022-07-21 09:50:21 -07:00
Brian Bowman
7f9ba66e38 Add lint:fix script 2022-07-19 20:47:11 -05:00
Brian Bowman
f260379fa2 Add formatting pre-commit hook 2022-07-19 20:47:11 -05:00
Brian Bowman
544c39168b Add prettier check to CI 2022-07-19 20:47:11 -05:00
Brian Bowman
eb9aa34323 Run prettier formatter 2022-07-19 20:47:11 -05:00
Brian Bowman
e9df0f17db Tweak .editorconfig and line endings
- Use `lf` line endings everywhere. This works fine even on Windows, and is more common for git repos. It also matches prettier's default.

For .editorconfig:

- Remove `insert_final_newline = false`: Omitting the final newline isn't supported by prettier or rustfmt.
- Remove `trim_trailing_whitespace = false`: The formatters will remove trailing whitespace anyways. I can't think of a good reason to want to keep trailing whitespace.
- Remove other redundant options.
2022-07-19 20:42:43 -05:00
Brian Bowman
bf2ae51fb0 Add prettier formatter 2022-07-19 20:42:43 -05:00
SpikeHD
dc20fe5916 set proxy addr only when launching with proxy 2022-07-19 17:52:17 -07:00
SpikeHD
1a6ed38f8f only unpatch when game was patched automatically 2022-07-19 17:49:40 -07:00
SpikeHD
d23d5e3806 version bump! 2022-07-19 17:45:13 -07:00
SpikeHD
365a4f2888 spanish translation 2022-07-19 17:43:31 -07:00
SpikeHD
e270c886db 3dm support that doesn't actually work I just don't wanna fix it 2022-07-19 17:40:22 -07:00
SpikeHD
18a1b0e94c Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-07-19 17:14:59 -07:00
SpikeHD
3799ec648d make proxy optional 2022-07-19 17:14:55 -07:00
SpikeHD
8ff06f6d29 make metadata optional 2022-07-19 17:07:10 -07:00
SpikeHD
6ff1ef932c Merge pull request #35 from ffauzan/patch-1
Fix toggle encryption
2022-07-19 09:11:59 -07:00
SpikeHD
9e29135376 Merge pull request #38 from ffauzan/patch-2
Fix patched metadata checking
2022-07-19 09:10:09 -07:00
ffauzan
109f98db66 Fix patched metadata checking
Issue: The function checked the wrong directory for global-metadata-patched.dat
2022-07-19 16:41:25 +07:00
ffauzan
69201bc8b1 Update file_helpers.rs 2022-07-19 11:52:12 +07:00
ffauzan
656fa2cfe3 Revert "fix conflict"
This reverts commit 1588bee5a3, reversing
changes made to 75b79d0202.
2022-07-19 11:47:38 +07:00
Ffauzan
1588bee5a3 fix conflict 2022-07-19 11:27:28 +07:00
Ffauzan
75b79d0202 Merge branch 'Grasscutters-main' into patch-1 2022-07-19 11:04:03 +07:00
Ffauzan
6d9f1af134 Merge branch 'main' of https://github.com/Grasscutters/Cultivation into Grasscutters-main 2022-07-19 11:03:51 +07:00
SpikeHD
d38459bb8a Merge pull request #37 from Seeker14491/patch-1
Fix lints, update deps, clean readme
2022-07-18 18:06:24 -07:00
ffauzan
c7954d2294 Update file_helpers.rs 2022-07-19 06:39:29 +07:00
Brian Bowman
adbb8e380d Clean up readme 2022-07-18 18:01:08 -05:00
Brian Bowman
4ff9e88185 Update dependencies 2022-07-18 18:01:08 -05:00
Brian Bowman
27a10c58ca Fix lints 2022-07-18 18:01:08 -05:00
ffauzan
44b148f2a4 Fix toggle encryption
Open the config file in write mode so the toggle encryption button is actually working
2022-07-18 22:03:01 +07:00
SpikeHD
6434814d1d fix decryption 2022-07-17 00:08:31 -07:00
SpikeHD
043f3e7ce4 simplify metadata processes 2022-07-16 23:34:55 -07:00
113 changed files with 6612 additions and 4016 deletions

View File

@@ -2,22 +2,9 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = crlf end_of_line = lf
indent_size = 2 indent_size = 2
indent_style = space indent_style = space
insert_final_newline = false
max_line_length = 120 max_line_length = 120
tab_width = 2 tab_width = 2
trim_trailing_whitespace = false trim_trailing_whitespace = true
[*.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

View File

@@ -1,59 +1,40 @@
{ {
"env": { "env": {
"browser": true, "browser": true,
"es2021": true, "es2021": true,
"node": true "node": true
},
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}, },
"extends": [ "ecmaVersion": "latest",
"eslint:recommended", "sourceType": "module"
"plugin:react/recommended", },
"plugin:@typescript-eslint/recommended" "plugins": ["react", "@typescript-eslint"],
], "rules": {
"parser": "@typescript-eslint/parser", "@typescript-eslint/ban-types": [
"parserOptions": { "warn",
"ecmaFeatures": { {
"jsx": true "extendDefaults": true,
}, "types": {
"ecmaVersion": "latest", "{}": false
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
2
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"never"
],
"@typescript-eslint/ban-types": [
"warn",
{
"extendDefaults": true,
"types": {
"{}": false
}
}
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
},
"settings": {
"react": {
"version": "detect"
} }
}
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
},
"settings": {
"react": {
"version": "detect"
} }
}
} }

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -3,12 +3,12 @@ name: Check backend
on: on:
push: push:
paths: paths:
- ".github/workflows/backend-checks.yml" - '.github/workflows/backend-checks.yml'
- "src-tauri/**" - 'src-tauri/**'
pull_request: pull_request:
paths: paths:
- ".github/workflows/backend-checks.yml" - '.github/workflows/backend-checks.yml'
- "src-tauri/**" - 'src-tauri/**'
concurrency: concurrency:
group: ${{ github.ref }}-${{ github.workflow }} group: ${{ github.ref }}-${{ github.workflow }}
@@ -59,4 +59,3 @@ jobs:
name: clippy (${{ runner.os }}) name: clippy (${{ runner.os }})
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
args: --manifest-path ./src-tauri/Cargo.toml --no-default-features -- -D warnings args: --manifest-path ./src-tauri/Cargo.toml --no-default-features -- -D warnings

View File

@@ -2,36 +2,31 @@ name: Check frontend
on: on:
push: push:
paths: paths-ignore:
- ".github/workflows/frontend-checks.yml" - '**.lock'
- "src/**" - '**.rs'
- ".eslintrc.json" - '**.toml'
- "package.json"
- "tsconfig.json"
- "yarn.lock"
pull_request: pull_request:
paths: paths-ignore:
- ".github/workflows/frontend-checks.yml" - '**.lock'
- "src/**" - '**.rs'
- ".eslintrc.json" - '**.toml'
- "package.json"
- "tsconfig.json"
- "yarn.lock"
concurrency: concurrency:
group: ${{ github.ref }}-${{ github.workflow }} group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
tsc-eslint-checks: prettier-tsc-eslint-checks:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install modules - name: Install modules
run: yarn run: yarn
- name: Run prettier
run: yarn prettier --check .
- name: Run tsc - name: Run tsc
run: yarn tsc --noEmit run: yarn tsc --noEmit
- name: Run ESLint - name: Run ESLint
run: yarn eslint src run: yarn eslint src

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged

4
.lintstagedrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"src-tauri/**/*.rs": "rustfmt --edition 2021",
"*": "yarn prettier --write --ignore-unknown"
}

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
src-tauri/resources/
src-tauri/target/
src-tauri/WixTools/

4
.prettierrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

View File

@@ -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. 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 # Cultivation
A game launcher designed to easily proxy traffic from anime game to private servers. 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. 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. Please do **NOT install, download, or use pre-compiled versions of Cultivation found elsewhere**. Only use releases from this GitHub repository.
# Table Of Contents # Table Of Contents
* [Download](#download)
* [Developer Quick-start](#developer-quickstart) - [Download](#download)
* [Setup](#setup) - [Developer Quick-start](#developer-quickstart)
* [Building](#building) - [Setup](#setup)
* [Troubleshooting](#troubleshooting) - [Building](#building)
* [Theming](#theming) - [Code Formatting and Linting](#code-formatting-and-linting)
- [Generating Update Artifacts](#generating-update-artifacts)
- [Theming](#theming)
- [Screenshots](#screenshots)
- [Credits](#credits)
# Download # Download
[Find release builds here!](https://github.com/Grasscutters/Cultivation/releases) [Find release builds here!](https://github.com/Grasscutters/Cultivation/releases)
Once downloaded, extract somewhere and open as administrator. Once downloaded, extract somewhere and open as administrator.
@@ -23,35 +30,47 @@ Once downloaded, extract somewhere and open as administrator.
# Developer Quickstart # Developer Quickstart
### Setup ### 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 [NodeJS >12](https://nodejs.org/en/)
* Install [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) & [Rust compiler](https://www.rust-lang.org/tools/install) - Install [yarn](https://classic.yarnpkg.com/lang/en/docs/install) (cry about it `npm` lovers)
* `yarn install` - Install [Rust](https://www.rust-lang.org/tools/install)
* `yarn start:dev` - `yarn install`
- `yarn start:dev`
### Building ### 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 ### Code Formatting and Linting
Format the code with `npm format` or `yarn format`. Run the lints with `npm lint` or `yarn lint`. Formatting:
### Updating - `yarn format`
* 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`
# Troubleshooting Check Lints, fix (some) lints:
TODO. Collect common issues before updating.
- `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 # Theming
A full theming reference can be found [here!](/THEMES.md) A full theming reference can be found [here!](/THEMES.md)
# Screenshots # Screenshots
![image](https://user-images.githubusercontent.com/25207995/173211603-e5e85df7-7fd3-430b-9246-749ebbc1e483.png) ![image](https://user-images.githubusercontent.com/25207995/173211603-e5e85df7-7fd3-430b-9246-749ebbc1e483.png)
![image](https://user-images.githubusercontent.com/25207995/173211543-b7e88943-cfd2-418b-ac48-7f856868129b.png) ![image](https://user-images.githubusercontent.com/25207995/173211543-b7e88943-cfd2-418b-ac48-7f856868129b.png)
![image](https://user-images.githubusercontent.com/25207995/173211561-a1778fdc-5cfe-4687-9a00-44500d29e470.png) ![image](https://user-images.githubusercontent.com/25207995/173211561-a1778fdc-5cfe-4687-9a00-44500d29e470.png)
@@ -59,8 +78,9 @@ A full theming reference can be found [here!](/THEMES.md)
![image](https://user-images.githubusercontent.com/25207995/173211590-6a2242b5-1e8f-4db9-a5c7-06284688b131.png) ![image](https://user-images.githubusercontent.com/25207995/173211590-6a2242b5-1e8f-4db9-a5c7-06284688b131.png)
## Credits ## 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. - [SpikeHD](https://github.com/SpikeHD): For originally creating **GrassClipper** and creating the amazing UI of Cultivation.
* [Benj](https://github.com/4Benj): For assistance in client patching. - [KingRainbow44](https://github.com/KingRainbow44): For building a proxy daemon from scratch and integrating it with Cultivation.
* [lilmayofuksu](https://github.com/lilmayofuksu): For assistance in client patching. - [Benj](https://github.com/4Benj): For assistance in client patching.
* [Tauri](https://tauri.app): For providing an amazing, efficient, and simple desktop application framework/library. - [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.

View File

@@ -2,7 +2,7 @@
1. Download your favorite theme! (You can find some in the `#themes` channel on Discord) 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`) 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 # Creating your own theme
@@ -16,16 +16,16 @@ 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: `index.json` is where you tell Cultivation which files and images to include. It supports the following properties:
| Property | Description | | Property | Description |
| :--- | :--- | | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | The name of the theme. | | `name` | The name of the theme. |
| `version` | Not shown anywhere, the version of the theme. | | `version` | Not shown anywhere, the version of the theme. |
| `description` | Not shown anywhere, the description of the theme. | | `description` | Not shown anywhere, the description of the theme. |
| `includes` | The files and folders to include. | | `includes` | The files and folders to include. |
| `includes.css` | Array of CSS files to include. Example: `css: ["index.css"]` | | `includes.css` | Array of CSS files to include. Example: `css: ["index.css"]` |
| `includes.js` | Array of JS files to includes. Example `js: ["index.js"]` | | `includes.js` | Array of JS files to includes. Example `js: ["index.js"]` |
| `customBackgroundURL` | A custom image URL to set as the background. Backgrounds that users set in their config supercede this. Example: `"https://website.com/image.png"` | | `customBackgroundURL` | A custom image URL to set as the background. Backgrounds that users set in their config supercede this. Example: `"https://website.com/image.png"` |
| `customBackgroundFile` | Path to a custom background image file. Backgrounds that users set in their config supercede this. Example: `"/image.png"` | | `customBackgroundFile` | Path to a custom background image file. Backgrounds that users set in their config supercede this. Example: `"/image.png"` |
A full, complete `index.json` will look something like this: A full, complete `index.json` will look something like this:
@@ -55,15 +55,17 @@ Below are some small examples of what you can do:
```css ```css
/* Change the font */ /* Change the font */
body { body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
} }
``` ```
```css ```css
/* Remove the news section */ /* Remove the news section */
.NewsSection { .NewsSection {
display: none; display: none;
} }
``` ```
```css ```css
/* Change the right bar width */ /* Change the right bar width */
.RightBar { .RightBar {
@@ -72,6 +74,7 @@ body {
``` ```
## How can I change XYZ element? ## 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. 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 ## Writing your JS
@@ -83,24 +86,26 @@ Below are some examples of what you can do:
```js ```js
/* Change the version number every 500ms */ /* Change the version number every 500ms */
setInterval(() => { setInterval(() => {
document.getElementById("version").innerHTML = "v" + Math.floor(Math.random() * 100); document.getElementById('version').innerHTML = 'v' + Math.floor(Math.random() * 100)
}, 500); }, 500)
``` ```
```js ```js
/* Load a custom font */ /* Load a custom font */
const head = document.head 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.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'
link.rel = "stylesheet" link.rel = 'stylesheet'
link.type = "text/css" link.type = 'text/css'
head.appendChild(link) head.appendChild(link)
``` ```
```js ```js
/* Create a new button that does nothing */ /* Create a new button that does nothing */
const newButton = document.createElement("button"); const newButton = document.createElement('button')
newButton.innerHTML = "New Button"; newButton.innerHTML = 'New Button'
document.body.appendChild(newButton); document.body.appendChild(newButton)
``` ```

View File

@@ -1,133 +1,135 @@
# Documentation of Element ID's and Classes for custom theming # Documentation of Element ID's and Classes for custom theming
## IDs ## 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. 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 | | #ID | Description |
|----------------------------------------|-----------------------------------------------------------------| | ------------------------------------ | --------------------------------------------------------------- |
| `#miniDialogContainer` | Main container of MiniDialog | | `#miniDialogContainer` | Main container of MiniDialog |
| `#miniDialogContainerTop` | Affects only top section of MiniDialog | | `#miniDialogContainerTop` | Affects only top section of MiniDialog |
| `#miniDialogButtonClose` | Close button (SVG) of MiniDialog | | `#miniDialogButtonClose` | Close button (SVG) of MiniDialog |
| `#miniDialogContent` | MiniDialog content | | `#miniDialogContent` | MiniDialog content |
| `#rightBarContainer` | Main container of RightBar | | `#rightBarContainer` | Main container of RightBar |
| `#rightBarContent` | RightBar content | | `#rightBarContent` | RightBar content |
| `#rightBarButtonDiscord` | Discord button on the RightBar | | `#rightBarButtonDiscord` | Discord button on the RightBar |
| `#rightBarButtonGithub` | Github button on the RightBar | | `#rightBarButtonGithub` | Github button on the RightBar |
| `#playButton` | Main container for whole launch buttons section | | `#playButton` | Main container for whole launch buttons section |
| `#serverControls` | Container of "play on grasscutter" checkbox | | `#serverControls` | Container of "play on grasscutter" checkbox |
| `#enableGC` | "play on grasscutter" checkbox | | `#enableGC` | "play on grasscutter" checkbox |
| `#ip` | Server ip input if play on grasscutter is enabled | | `#ip` | Server ip input if play on grasscutter is enabled |
| `#port` | Server port input if play on grasscutter is enabled | | `#port` | Server port input if play on grasscutter is enabled |
| `#httpsEnable` | "Enable https" checkbox if play on grasscutter is enabled | | `#httpsEnable` | "Enable https" checkbox if play on grasscutter is enabled |
| `#officialPlay` | Launch button | | `#officialPlay` | Launch button |
| `#serverLaunch` | Launch server button | | `#serverLaunch` | Launch server button |
| `#serverlaunchIcon` | Icon (SVG) of server launch button | | `#serverlaunchIcon` | Icon (SVG) of server launch button |
| `#serverConfigContainer` | Main container of server configuration section | | `#serverConfigContainer` | Main container of server configuration section |
| `#serverLaunchContainer` | Main container of launch buttons (includes launch server) | | `#serverLaunchContainer` | Main container of launch buttons (includes launch server) |
| `#topBarContainer` | Main container of launcher TopBar (minimize, exit, settings...) | | `#topBarContainer` | Main container of launcher TopBar (minimize, exit, settings...) |
| `#title` | Title of the TopBar | | `#title` | Title of the TopBar |
| `#version` | Version of the launcher in TopBar | | `#version` | Version of the launcher in TopBar |
| `#topBarButtonContainer` | Container of launcher TopBar buttons only | | `#topBarButtonContainer` | Container of launcher TopBar buttons only |
| `#closeBtn` | Exit launcher button | | `#closeBtn` | Exit launcher button |
| `#minBtn` | Minimize launcher button | | `#minBtn` | Minimize launcher button |
| `#settingsBtn` | Settings button | | `#settingsBtn` | Settings button |
| `#downloadsBtn` | Downloads button (grasscutter resources, grasscutter...) | | `#downloadsBtn` | Downloads button (grasscutter resources, grasscutter...) |
| `#newsContainer` | Main container of the news section | | `#newsContainer` | Main container of the news section |
| `#newsTabsContainer` | Container for news tabs | | `#newsTabsContainer` | Container for news tabs |
| `#commits` | News tabs container commits button | | `#commits` | News tabs container commits button |
| `#latest_version` | News tabs for latest version button | | `#latest_version` | News tabs for latest version button |
| `#newsContent` | Content section of news container | | `#newsContent` | Content section of news container |
| `#newsCommitsTable` | Commits table of news section | | `#newsCommitsTable` | Commits table of news section |
| `#downloadMenuContainerGCStable` | Grasscutter stable update container | | `#downloadMenuContainerGCStable` | Grasscutter stable update container |
| `#downloadMenuLabelGCStable` | Label for stable update button | | `#downloadMenuLabelGCStable` | Label for stable update button |
| `#downloadMenuButtonGCStable` | Button container for stable update button | | `#downloadMenuButtonGCStable` | Button container for stable update button |
| `#grasscutterStableBtn` | "Update grasscutter stable" button | | `#grasscutterStableBtn` | "Update grasscutter stable" button |
| `#downloadMenuContainerGCDev` | Grasscutter development update container | | `#downloadMenuContainerGCDev` | Grasscutter development update container |
| `#downloadMenuLabelGCDev` | Label for latest update button | | `#downloadMenuLabelGCDev` | Label for latest update button |
| `#downloadMenuButtonGCDev` | Button container for latest update button | | `#downloadMenuButtonGCDev` | Button container for latest update button |
| `grasscutterLatestBtn` | "Update grasscutter latest" button | | `grasscutterLatestBtn` | "Update grasscutter latest" button |
| `#downloadMenuContainerGCStableData` | Grasscutter stable data update container | | `#downloadMenuContainerGCStableData` | Grasscutter stable data update container |
| `#downloadMenuLabelGCStableData` | Label for stable data update | | `#downloadMenuLabelGCStableData` | Label for stable data update |
| `#downloadMenuButtonGCStableData` | Button container for stable data update button | | `#downloadMenuButtonGCStableData` | Button container for stable data update button |
| `#grasscutterStableRepo` | "Update grasscutter stable data" button | | `#grasscutterStableRepo` | "Update grasscutter stable data" button |
| `#downloadMenuContainerGCDevData` | Grasscutter latest data update container | | `#downloadMenuContainerGCDevData` | Grasscutter latest data update container |
| `#downloadMenuLabelGCDevData` | Label for latest data update | | `#downloadMenuLabelGCDevData` | Label for latest data update |
| `#downloadMenuButtonGCDevData` | Button container for latest data update button | | `#downloadMenuButtonGCDevData` | Button container for latest data update button |
| `#grasscutterDevRepo` | "Update grasscutter latest data" button | | `#grasscutterDevRepo` | "Update grasscutter latest data" button |
| `#downloadMenuContainerResources` | Container for grasscutter resources download | | `#downloadMenuContainerResources` | Container for grasscutter resources download |
| `#downloadMenuLabelResources` | label for resources download | | `#downloadMenuLabelResources` | label for resources download |
| `#downloadMenuButtonResources` | Button container for resources download button | | `#downloadMenuButtonResources` | Button container for resources download button |
| `#resourcesBtn` | "Download grasscutter resources" button | | `#resourcesBtn` | "Download grasscutter resources" button |
| `#menuContainer` | Generic Popup modal like menu container | | `#menuContainer` | Generic Popup modal like menu container |
| `#menuContainerTop` | Top section of menu container | | `#menuContainerTop` | Top section of menu container |
| `#menuHeading` | Menu title | | `#menuHeading` | Menu title |
| `#menuButtonCloseContainer` | Container for menu close button | | `#menuButtonCloseContainer` | Container for menu close button |
| `#menuButtonCloseIcon` | Menu close icon (SVG) | | `#menuButtonCloseIcon` | Menu close icon (SVG) |
| `#menuContent` | Content section of the menu | | `#menuContent` | Content section of the menu |
| `#menuOptionsContainerGameExec` | Container for game executable option section | | `#menuOptionsContainerGameExec` | Container for game executable option section |
| `#menuOptionsLabelGameExec` | Label for game executable option | | `#menuOptionsLabelGameExec` | Label for game executable option |
| `#menuOptionsDirGameExec` | Set game executable file browser | | `#menuOptionsDirGameExec` | Set game executable file browser |
| `#menuOptionsContainerGCJar` | Container for grasscutter jar option | | `#menuOptionsContainerGCJar` | Container for grasscutter jar option |
| `#menuOptionsLabelGCJar` | Label for grasscutter jar option | | `#menuOptionsLabelGCJar` | Label for grasscutter jar option |
| `#menuOptionsDirGCJar` | Set grasscutter jar file browser | | `#menuOptionsDirGCJar` | Set grasscutter jar file browser |
| `#menuOptionsContainerToggleEnc` | Container for toggle encryption option | | `#menuOptionsContainerToggleEnc` | Container for toggle encryption option |
| `#menuOptionsLabelToggleEnc` | Label for toggle encryption option | | `#menuOptionsLabelToggleEnc` | Label for toggle encryption option |
| `#menuOptionsButtonToggleEnc` | Toggle encryption button container | | `#menuOptionsButtonToggleEnc` | Toggle encryption button container |
| `#toggleEnc` | Toggle encryption button | | `#toggleEnc` | Toggle encryption button |
| `#menuOptionsContainerGCWGame` | Container for "grasscutter with game" option | | `#menuOptionsContainerGCWGame` | Container for "grasscutter with game" option |
| `#menuOptionsLabelGCWDame` | Label for "grasscutter with game" option | | `#menuOptionsLabelGCWDame` | Label for "grasscutter with game" option |
| `#menuOptionsCheckboxGCWGame` | Container for "grasscutter with game" option checkbox | | `#menuOptionsCheckboxGCWGame` | Container for "grasscutter with game" option checkbox |
| `#gcWithGame` | Grasscutter with game checkbox | | `#gcWithGame` | Grasscutter with game checkbox |
| `#menuOptionsContainerThemes` | Container for themes section | | `#menuOptionsContainerThemes` | Container for themes section |
| `#menuOptionsLabelThemes` | Label for set themes option | | `#menuOptionsLabelThemes` | Label for set themes option |
| `#menuOptionsSelectThemes` | Container for themes select menu | | `#menuOptionsSelectThemes` | Container for themes select menu |
| `#menuOptionsSelectMenuThemes` | Set theme select menu | | `#menuOptionsSelectMenuThemes` | Set theme select menu |
| `#menuOptionsContainerJavaPath` | Container for Java Path option | | `#menuOptionsContainerJavaPath` | Container for Java Path option |
| `#menuOptionsLabelJavaPath` | Label for Java path option | | `#menuOptionsLabelJavaPath` | Label for Java path option |
| `#menuOptionsDirJavaPath` | Container for java path file browser | | `#menuOptionsDirJavaPath` | Container for java path file browser |
| `#menuOptionsContainerBG` | Container for Background option | | `#menuOptionsContainerBG` | Container for Background option |
| `#menuOptionsLabelBG` | Label for background option | | `#menuOptionsLabelBG` | Label for background option |
| `#menuOptionsDirBG` | Container for background url/local path option | | `#menuOptionsDirBG` | Container for background url/local path option |
| `#menuOptionsContainerLang` | Container for language change option | | `#menuOptionsContainerLang` | Container for language change option |
| `#menuOptionsLabelLang` | Label for language change option | | `#menuOptionsLabelLang` | Label for language change option |
| `#menuOptionsSelectLang` | Container for language change select menu | | `#menuOptionsSelectLang` | Container for language change select menu |
| `#menuOptionsSelectMenuLang` | Language select menu | | `#menuOptionsSelectMenuLang` | Language select menu |
| `#DownloadProgress` | Download progress container | | `#DownloadProgress` | Download progress container |
| `#bottomSectionContainer` | Bottom section container | | `#bottomSectionContainer` | Bottom section container |
| `#miniDownloadContainer` | Container for mini download | | `#miniDownloadContainer` | Container for mini download |
## Classes ## 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. 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 | | .Class | Description |
|-----------------------------|---------------------------------------------------------| | ------------------------- | ------------------------------------------------------- |
| `.BigButton` | Class for all buttons | | `.BigButton` | Class for all buttons |
| `.BigButtonText` | Text inside a button | | | `.BigButtonText` | Text inside a button |
| `.Checkbox` | Checkbox container | | `.Checkbox` | Checkbox container |
| `.CheckboxDisplay` | Content of checkbox | | `.CheckboxDisplay` | Content of checkbox |
| `.DirInput` | Container for DirInput | | `.DirInput` | Container for DirInput |
| `.FileSelectIcon` | Icon of DirInput | | `.FileSelectIcon` | Icon of DirInput |
| `.DownloadList` | List of all downloads | | `.DownloadList` | List of all downloads |
| `.DownloadSection` | Container for each download | | `.DownloadSection` | Container for each download |
| `.DownloadTitle` | Contains file download path and current status | | `.DownloadTitle` | Contains file download path and current status |
| `.DownloadPath` | Path of a download | | `.DownloadPath` | Path of a download |
| `.DownloadStatus` | Status of a download | | `.DownloadStatus` | Status of a download |
| `.DownloadSectionInner` | Contains progressbar of the download section | | `.DownloadSectionInner` | Contains progressbar of the download section |
| `.HelpSection` | Container for help "?" circle button | | `.HelpSection` | Container for help "?" circle button |
| `.HelpButton` | HelpButton itself | | `.HelpButton` | HelpButton itself |
| `.HelpContents` | Content of help button once expanded | | `.HelpContents` | Content of help button once expanded |
| `.MainProgressBarWrapper` | Container for MainProgressBar | | `.MainProgressBarWrapper` | Container for MainProgressBar |
| `.ProgressBar` | ProgressBar (creativity left the brain) | | `.ProgressBar` | ProgressBar (creativity left the brain) |
| `.InnerProgress` | ProgressBar percentage | | `.InnerProgress` | ProgressBar percentage |
| `.MainProgressText` | Text for MainProgressBar | | `.MainProgressText` | Text for MainProgressBar |
| `.ProgressBarWrapper` | Container for ProgressBar | | `.ProgressBarWrapper` | Container for ProgressBar |
| `.DownloadControls` | DownloadControls of ProgressBar | | `.DownloadControls` | DownloadControls of ProgressBar |
| `.downloadStop` | Container for download stop icon (SVG) | | `.downloadStop` | Container for download stop icon (SVG) |
| `.ProgressText` | Text of the ProgressBar display current download status | | `.ProgressText` | Text of the ProgressBar display current download status |
| `.TextInputWrapper` | Container for TextInput | | `.TextInputWrapper` | Container for TextInput |
| `.TextClear` | Container for clear input content button | | `.TextClear` | Container for clear input content button |
| `.TextInputClear` | TextInput clear button icon (SVG) | | `.TextInputClear` | TextInput clear button icon (SVG) |
| `.Divider` | Container for line dividers | | `.Divider` | Container for line dividers |
| `.DividerLine` | Divider line itself | | `.DividerLine` | Divider line itself |
| `.CommitAuthor` | Author of a commit | | `.CommitAuthor` | Author of a commit |
| `.CommitMessage` | Message of a commit | | `.CommitMessage` | Message of a commit |

View File

@@ -1,15 +1,19 @@
# Troubleshooting # Troubleshooting
A guide dedicated for trying to troubleshoot Cultivation. A guide dedicated for trying to troubleshoot Cultivation.
## The launcher doesn't appear to open. ## The launcher doesn't appear to open.
Try running the launcher with **administrative privileges**.\ Try running the launcher with **administrative privileges**.\
If this fixes your issue, you can force enable it in the **Compatability**\ If this fixes your issue, you can force enable it in the **Compatability**\
tab for the launcher's executable. tab for the launcher's executable.
## Unable to play on `localhost`. ## Unable to play on `localhost`.
Make sure your server is running with **encryption disabled** and `useInRouting` to **false**.\ Make sure your server is running with **encryption disabled** and `useInRouting` to **false**.\
Additionally, make sure Cultivation **is set to not use HTTPS**. Additionally, make sure Cultivation **is set to not use HTTPS**.
## "I can't do anything requiring the internet after closing Cultivation!" ## "I can't do anything requiring the internet after closing Cultivation!"
You probably didn't close Cultivation properly.\ 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.

View File

@@ -1,14 +1,14 @@
{ {
"name": "cultivation", "name": "cultivation",
"version": "1.0.3", "version": "1.0.6",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.0.0-rc.5", "@tauri-apps/api": "^1.0.0-rc.5",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0", "@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1", "@testing-library/user-event": "^14.2.6",
"@types/jest": "^27.0.1", "@types/jest": "^28.1.6",
"@types/node": "^16.7.13", "@types/node": "^18.0.6",
"@types/react": "^18.0.0", "@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"react": "^18.1.0", "react": "^18.1.0",
@@ -28,8 +28,10 @@
"eject": "react-scripts eject", "eject": "react-scripts eject",
"tauri": "tauri", "tauri": "tauri",
"start:dev": "tauri dev", "start:dev": "tauri dev",
"format": "cargo fmt --manifest-path ./src-tauri/Cargo.toml --all", "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": "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": { "eslintConfig": {
"extends": [ "extends": [
@@ -55,7 +57,11 @@
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.22.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.15.0", "eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.29.4", "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" "run-script-os": "^1.1.6"
} }
} }

View File

@@ -5,10 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Tauri-powered anime game launcher" />
name="description"
content="Tauri-powered anime game launcher"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Cultivation</title> <title>Cultivation</title>

318
src-tauri/Cargo.lock generated
View File

@@ -75,7 +75,7 @@ dependencies = [
"asn1-rs-impl", "asn1-rs-impl",
"displaydoc", "displaydoc",
"nom", "nom",
"num-traits", "num-traits 0.2.15",
"rusticata-macros", "rusticata-macros",
"thiserror", "thiserror",
"time 0.3.11", "time 0.3.11",
@@ -506,9 +506,9 @@ dependencies = [
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "1.2.2" version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" checksum = "83827793632c72fa4f73c2edb31e7a997527dd8ffe7077344621fc62c5478157"
dependencies = [ dependencies = [
"cache-padded", "cache-padded",
] ]
@@ -657,9 +657,9 @@ dependencies = [
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.4" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"typenum", "typenum",
@@ -719,9 +719,8 @@ dependencies = [
"http", "http",
"hudsucker", "hudsucker",
"is_elevated", "is_elevated",
"libloading",
"once_cell", "once_cell",
"open 2.1.3", "open",
"rcgen", "rcgen",
"regex", "regex",
"registry", "registry",
@@ -737,6 +736,7 @@ dependencies = [
"tokio-rustls", "tokio-rustls",
"tokio-tungstenite", "tokio-tungstenite",
"tracing", "tracing",
"unrar",
"zip 0.6.2", "zip 0.6.2",
"zip-extract", "zip-extract",
] ]
@@ -784,9 +784,9 @@ checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]] [[package]]
name = "dbus" name = "dbus"
version = "0.9.5" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce" checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6"
dependencies = [ dependencies = [
"libc", "libc",
"libdbus-sys", "libdbus-sys",
@@ -821,8 +821,8 @@ dependencies = [
"asn1-rs", "asn1-rs",
"displaydoc", "displaydoc",
"nom", "nom",
"num-bigint", "num-bigint 0.4.3",
"num-traits", "num-traits 0.2.15",
"rusticata-macros", "rusticata-macros",
] ]
@@ -949,6 +949,15 @@ dependencies = [
"cfg-if 1.0.0", "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]] [[package]]
name = "error-chain" name = "error-chain"
version = "0.12.4" version = "0.12.4"
@@ -1042,6 +1051,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@@ -1239,15 +1254,15 @@ dependencies = [
[[package]] [[package]]
name = "generator" name = "generator"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee" checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
"log", "log",
"rustversion", "rustversion",
"winapi", "windows 0.32.0",
] ]
[[package]] [[package]]
@@ -1463,9 +1478,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.2" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -1700,8 +1715,8 @@ dependencies = [
"byteorder", "byteorder",
"color_quant", "color_quant",
"num-iter", "num-iter",
"num-rational", "num-rational 0.4.1",
"num-traits", "num-traits 0.2.15",
] ]
[[package]] [[package]]
@@ -1887,16 +1902,6 @@ dependencies = [
"pkg-config", "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]] [[package]]
name = "line-wrap" name = "line-wrap"
version = "0.1.1" version = "0.1.1"
@@ -1948,9 +1953,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "mac-notification-sys" name = "mac-notification-sys"
version = "0.5.2" version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "042f74a606175d72ca483e14e0873fe0f6c003f7af45865b17b16fdaface7203" checksum = "fff231a88fe2e9985f9d159a2f02986fe46daa0f6af976a0d934be4870cc9d02"
dependencies = [ dependencies = [
"cc", "cc",
"dirs-next", "dirs-next",
@@ -2182,6 +2187,32 @@ dependencies = [
"winapi", "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]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.3" version = "0.4.3"
@@ -2190,7 +2221,17 @@ checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"num-integer", "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]] [[package]]
@@ -2200,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"num-traits", "num-traits 0.2.15",
] ]
[[package]] [[package]]
@@ -2211,7 +2252,19 @@ checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"num-integer", "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]] [[package]]
@@ -2222,7 +2275,16 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"num-integer", "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]] [[package]]
@@ -2336,19 +2398,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "open" name = "open"
version = "2.1.3" version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2423ffbf445b82e58c3b1543655968923dd06f85432f10be2bb4f1b7122f98c" checksum = "f23a407004a1033f53e93f9b45580d14de23928faad187384f891507c9b0c045"
dependencies = [
"pathdiff",
"windows-sys",
]
[[package]]
name = "open"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "360bcc8316bf6363aa3954c3ccc4de8add167b087e0259190a043c9514f910fe"
dependencies = [ dependencies = [
"pathdiff", "pathdiff",
"windows-sys", "windows-sys",
@@ -2356,9 +2408,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.40" version = "0.10.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@@ -2388,9 +2440,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.74" version = "0.9.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
@@ -2546,9 +2598,9 @@ dependencies = [
[[package]] [[package]]
name = "pem" name = "pem"
version = "1.0.2" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
dependencies = [ dependencies = [
"base64", "base64",
] ]
@@ -2852,6 +2904,19 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rand" name = "rand"
version = "0.7.3" version = "0.7.3"
@@ -2897,6 +2962,21 @@ dependencies = [
"rand_core 0.6.3", "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]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.5.1" version = "0.5.1"
@@ -2977,9 +3057,9 @@ dependencies = [
[[package]] [[package]]
name = "rcgen" name = "rcgen"
version = "0.9.2" version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7fa2d386df8533b02184941c76ae2e0d0c1d053f5d43339169d80f21275fc5e" checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd"
dependencies = [ dependencies = [
"pem", "pem",
"ring", "ring",
@@ -2988,6 +3068,15 @@ dependencies = [
"yasna", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.13" version = "0.2.13"
@@ -3134,6 +3223,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "rustc-serialize"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.3.3" version = "0.3.3"
@@ -3184,9 +3279,9 @@ dependencies = [
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.7" version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8"
[[package]] [[package]]
name = "ryu" name = "ryu"
@@ -3322,18 +3417,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.138" version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.138" version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3789,9 +3884,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61fc211e0bd2c04c0aecd202d2cd72dd797a89da02989a39e1b9691462386d6" checksum = "827f61bd3dd40276694be5c7ffc40d65b94ab00d9f8c1a4a4db07f2cdc306c83"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"attohttpc", "attohttpc",
@@ -3810,7 +3905,7 @@ dependencies = [
"notify-rust", "notify-rust",
"objc", "objc",
"once_cell", "once_cell",
"open 3.0.1", "open",
"os_info", "os_info",
"os_pipe 1.0.1", "os_pipe 1.0.1",
"percent-encoding", "percent-encoding",
@@ -3842,9 +3937,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f2b32e551ec810ba4ab2ad735de5e3576e54bf0322ab0f4b7ce41244bc65ecf" checksum = "acafb1c515c5d14234a294461bd43c723639a84891a45f6a250fd3441ad2e8ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -3858,9 +3953,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f1f7928dd040fc03c94207adfad506c0cf5b152982fd1dc0a621f7fd777e22" checksum = "16d62a3c8790d6cba686cea6e3f7f569d12c662c3274c2d165a4fd33e3871b72"
dependencies = [ dependencies = [
"base64", "base64",
"brotli", "brotli",
@@ -3884,9 +3979,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50b9f52871c088857360319a37472d59f4644f1ed004489599d62831a1b6996" checksum = "7296fa17996629f43081e1c66d554703900187ed900c5bf46f97f0bcfb069278"
dependencies = [ dependencies = [
"heck 0.4.0", "heck 0.4.0",
"proc-macro2", "proc-macro2",
@@ -4068,10 +4163,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.19.2" version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e"
dependencies = [ dependencies = [
"autocfg",
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
@@ -4107,9 +4203,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.17.1" version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06cda1232a49558c46f8a504d5b93101d42c0bf7f911f12a105ba48168f821ae" checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@@ -4236,9 +4332,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.17.2" version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5" checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
dependencies = [ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
@@ -4284,9 +4380,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
@@ -4309,6 +4405,31 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" 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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"
@@ -4690,6 +4811,19 @@ dependencies = [
"windows_x86_64_msvc 0.24.0", "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]] [[package]]
name = "windows" name = "windows"
version = "0.37.0" version = "0.37.0"
@@ -4749,6 +4883,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169" checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169"
[[package]]
name = "windows_aarch64_msvc"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.36.1" version = "0.36.1"
@@ -4767,6 +4907,12 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0866510a3eca9aed73a077490bbbf03e5eaac4e1fd70849d89539e5830501fd" checksum = "c0866510a3eca9aed73a077490bbbf03e5eaac4e1fd70849d89539e5830501fd"
[[package]]
name = "windows_i686_gnu"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.36.1" version = "0.36.1"
@@ -4785,6 +4931,12 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0ffed56b7e9369a29078d2ab3aaeceea48eb58999d2cff3aa2494a275b95c6" checksum = "bf0ffed56b7e9369a29078d2ab3aaeceea48eb58999d2cff3aa2494a275b95c6"
[[package]]
name = "windows_i686_msvc"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.36.1" version = "0.36.1"
@@ -4803,6 +4955,12 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384a173630588044205a2993b6864a2f56e5a8c1e7668c07b93ec18cf4888dc4" 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]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.36.1" version = "0.36.1"
@@ -4821,6 +4979,12 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd8f062d8ca5446358159d79a90be12c543b3a965c847c8f3eedf14b321d399" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.36.1" version = "0.36.1"

View File

@@ -31,13 +31,14 @@ sysinfo = "0.24.6"
# ZIP-archive library. # ZIP-archive library.
zip-extract = "0.1.1" zip-extract = "0.1.1"
unrar = "0.4.4"
zip = "0.6.2" zip = "0.6.2"
# For creating a "global" downloads list. # For creating a "global" downloads list.
once_cell = "1.13.0" once_cell = "1.13.0"
# Program opener. # Program opener.
open = "2.1.2" open = "3.0.2"
duct = "0.13.5" duct = "0.13.5"
# Serialization. # Serialization.
@@ -56,7 +57,6 @@ futures-util = "0.3.14"
rcgen = { version = "0.9", features = ["x509-parser"] } rcgen = { version = "0.9", features = ["x509-parser"] }
# metadata stuff # metadata stuff
libloading = "0.7"
regex = "1" regex = "1"
# other # other

View File

@@ -2,17 +2,14 @@ fn main() {
cc::Build::new() cc::Build::new()
.include("mhycrypto") .include("mhycrypto")
.cpp(true) .cpp(true)
.file("mhycrypto/memecrypto.cpp") .file("mhycrypto/memecrypto.cpp")
.file("mhycrypto/metadata.cpp") .file("mhycrypto/metadata.cpp")
.file("mhycrypto/metadatastringdec.cpp") .file("mhycrypto/metadatastringdec.cpp")
.compile("mhycrypto"); .compile("mhycrypto");
cc::Build::new() cc::Build::new()
.include("mhycrypto") .include("mhycrypto")
.file("mhycrypto/aes.c") .file("mhycrypto/aes.c")
.compile("mhycrypto-aes"); .compile("mhycrypto-aes");
tauri_build::build() tauri_build::build()

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -11,13 +11,21 @@
"files_extracting": "文件解压中:" "files_extracting": "文件解压中:"
}, },
"options": { "options": {
"enabled": "已启用",
"disabled": "已禁用",
"game_path": "选择游戏安装路径",
"game_executable": "选择游戏可执行文件", "game_executable": "选择游戏可执行文件",
"recover_metadata": "紧急情况下恢复元数据文件",
"grasscutter_jar": "选择 Grasscutter JAR 文件", "grasscutter_jar": "选择 Grasscutter JAR 文件",
"java_path": "设置自定义 Java 路径", "toggle_encryption": "启用加密",
"install_certificate": "安装代理证书",
"java_path": "选择自定义 Java 路径",
"grasscutter_with_game": "随游戏自动启动 Grasscutter", "grasscutter_with_game": "随游戏自动启动 Grasscutter",
"language": "语言", "language": "选择语言",
"background": "设置自定义背景(链接或文件)", "background": "设置自定义背景(链接或文件)",
"theme": "设置主题" "theme": "设置主题",
"patch_metadata": "自动修改元数据",
"use_proxy": "使用内置代理"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "下载 Grasscutter 稳定版数据", "grasscutter_stable_data": "下载 Grasscutter 稳定版数据",
@@ -28,7 +36,8 @@
"grasscutter_latest": "下载 Grasscutter 开发版", "grasscutter_latest": "下载 Grasscutter 开发版",
"grasscutter_stable_update": "更新 Grasscutter 稳定版", "grasscutter_stable_update": "更新 Grasscutter 稳定版",
"grasscutter_latest_update": "更新 Grasscutter 开发版", "grasscutter_latest_update": "更新 Grasscutter 开发版",
"resources": "下载 Grasscutter 资源" "resources": "下载 Grasscutter 资源",
"game": "下载游戏"
}, },
"download_status": { "download_status": {
"downloading": "下载中", "downloading": "下载中",
@@ -40,10 +49,11 @@
"components": { "components": {
"select_file": "选择文件或文件夹...", "select_file": "选择文件或文件夹...",
"select_folder": "选择文件夹...", "select_folder": "选择文件夹...",
"download": "下载" "download": "下载",
"install": "安装"
}, },
"news": { "news": {
"latest_commits": "最近的PR", "latest_commits": "最近提交",
"latest_version": "最新版本" "latest_version": "最新版本"
}, },
"help": { "help": {
@@ -53,6 +63,17 @@
"gc_dev_jar": "下载最新的 Grasscutter 开发版,包括 JAR 文件和数据。", "gc_dev_jar": "下载最新的 Grasscutter 开发版,包括 JAR 文件和数据。",
"gc_stable_data": "下载当前的 Grasscutter 稳定版数据,不包括 JAR 文件。此选项在更新时有帮助。", "gc_stable_data": "下载当前的 Grasscutter 稳定版数据,不包括 JAR 文件。此选项在更新时有帮助。",
"gc_dev_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 注入器"
} }
} }

View File

@@ -13,14 +13,19 @@
"options": { "options": {
"enabled": "已啟用", "enabled": "已啟用",
"disabled": "未啟用", "disabled": "未啟用",
"game_path": "選擇遊戲安裝路徑",
"game_executable": "選擇遊戲執行檔", "game_executable": "選擇遊戲執行檔",
"recover_metadata": "緊急恢復Metadata",
"grasscutter_jar": "選擇伺服器JAR檔案", "grasscutter_jar": "選擇伺服器JAR檔案",
"toggle_encryption": "設定加密", "toggle_encryption": "設定加密",
"java_path": "設定自定義Java路徑", "install_certificate": "安裝代理憑證",
"java_path": "選擇自定義Java路徑",
"grasscutter_with_game": "伴隨遊戲一起啟動Grasscutter", "grasscutter_with_game": "伴隨遊戲一起啟動Grasscutter",
"language": "語言", "language": "語言",
"background": "設定自定義背景(網址或檔案)", "background": "選擇自定義背景(網址或檔案)",
"theme": "設定主題" "theme": "選擇主題",
"patch_metadata": "自動修補Metadata",
"use_proxy": "使用內建代理伺服器"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "下載Grasscutter穩定版數據Data", "grasscutter_stable_data": "下載Grasscutter穩定版數據Data",
@@ -31,7 +36,8 @@
"grasscutter_latest": "下載Grasscutter開發板", "grasscutter_latest": "下載Grasscutter開發板",
"grasscutter_stable_update": "更新Grasscutter穩定版", "grasscutter_stable_update": "更新Grasscutter穩定版",
"grasscutter_latest_update": "更新Grasscutter開發板", "grasscutter_latest_update": "更新Grasscutter開發板",
"resources": "下載Grasscutter資源Resources" "resources": "下載Grasscutter資源Resources",
"game": "下載遊戲"
}, },
"download_status": { "download_status": {
"downloading": "下載中", "downloading": "下載中",
@@ -43,7 +49,8 @@
"components": { "components": {
"select_file": "選擇檔案或資料夾...", "select_file": "選擇檔案或資料夾...",
"select_folder": "選擇資料夾...", "select_folder": "選擇資料夾...",
"download": "下載" "download": "下載",
"install": "安裝"
}, },
"news": { "news": {
"latest_commits": "最近的PR", "latest_commits": "最近的PR",
@@ -51,11 +58,23 @@
}, },
"help": { "help": {
"port_help_text": "確保這是Dispatch伺服器端口不是遊戲伺服器端口。 大部分伺服器的端口都是443。", "port_help_text": "確保這是Dispatch伺服器端口不是遊戲伺服器端口。 大部分伺服器的端口都是443。",
"game_help_text": "不需要另外一個遊戲備份來使用Grasscutter。這是給想要降級到2.6或者還沒安裝遊戲的人使用的。", "game_help_text": "不需要另外一個遊戲備份來使用Grasscutter。這是給想要降級到2.6或者還沒安裝遊戲的人使用的。",
"gc_stable_jar": "下載當前的Grasscutter穩定版本包括JAR答案還有資料文件。", "gc_stable_jar": "下載當前的Grasscutter穩定版本包括JAR答案還有資料文件。",
"gc_dev_jar": "下載當前的Grasscutter穩定版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。", "gc_dev_jar": "下載當前的Grasscutter穩定版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。",
"gc_stable_data": "下載當前最新的Grasscutter開發版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。", "gc_stable_data": "下載當前最新的Grasscutter開發版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。",
"gc_dev_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注入器"
} }
} }

View File

@@ -1,61 +1,77 @@
{ {
"lang_name": "Deutsch", "lang_name": "Deutsch",
"main": { "main": {
"title": "Cultivation", "title": "Cultivation",
"launch_button": "Starten", "launch_button": "Starten",
"gc_enable": "Über Grasscutter verbinden", "gc_enable": "Über Grasscutter verbinden",
"https_enable": "HTTPS nutzen", "https_enable": "HTTPS nutzen",
"ip_placeholder": "Server Adresse...", "ip_placeholder": "Server Adresse...",
"port_placeholder": "Port...", "port_placeholder": "Port...",
"files_downloading": "Herunterladen von Dateien: ", "files_downloading": "Herunterladen von Dateien: ",
"files_extracting": "Extrahieren von Dateien: " "files_extracting": "Extrahieren von Dateien: "
}, },
"options": { "options": {
"enabled": "Aktiviert", "enabled": "Aktiviert",
"disabled": "Deaktiviert", "disabled": "Deaktiviert",
"game_executable": "Spiel Datei auswählen", "game_path": "Spielpfad",
"grasscutter_jar": "Grasscuter JAR auswählen", "game_executable": "Spiel Datei auswählen",
"toggle_encryption": "Verschlüsselung umschalten", "recover_metadata": "Notfall Wiederherstellung der Metadaten",
"java_path": "Benutzerdefinierten Java Pfad setzen", "grasscutter_jar": "Grasscuter JAR auswählen",
"grasscutter_with_game": "Grasscutter automatisch mit dem Spiel starten", "toggle_encryption": "Verschlüsselung umschalten",
"language": "Sprache auswählen", "java_path": "Benutzerdefinierten Java Pfad setzen",
"background": "Benutzerdefinierten Hintergrund festlegen (link oder bild)", "grasscutter_with_game": "Grasscutter automatisch mit dem Spiel starten",
"theme": "Theme auswählen" "language": "Sprache auswählen",
}, "background": "Benutzerdefinierten Hintergrund festlegen (link oder bild)",
"downloads": { "theme": "Theme auswählen",
"grasscutter_stable_data": "Stabile Grasscutter Daten herunterladen", "patch_metadata": "Metadaten automatisch patchen"
"grasscutter_latest_data": "Aktuellste Grasscutter Daten herunterladen", },
"grasscutter_stable_data_update": "Stabile Grasscutter Daten aktualisieren", "downloads": {
"grasscutter_latest_data_update": "Aktuellste Grasscutter Daten aktualisieren", "grasscutter_stable_data": "Stabile Grasscutter Daten herunterladen",
"grasscutter_stable": "Stabile Grasscutter Version herunterladen", "grasscutter_latest_data": "Aktuellste Grasscutter Daten herunterladen",
"grasscutter_latest": "Aktuellste Grasscutter Version herunterladen", "grasscutter_stable_data_update": "Stabile Grasscutter Daten aktualisieren",
"grasscutter_stable_update": "Stabile Grasscutter Version aktualisieren", "grasscutter_latest_data_update": "Aktuellste Grasscutter Daten aktualisieren",
"grasscutter_latest_update": "Aktuellste Grasscutter Version aktualisieren", "grasscutter_stable": "Stabile Grasscutter Version herunterladen",
"resources": "Grasscutter Ressourcen herunterladen" "grasscutter_latest": "Aktuellste Grasscutter Version herunterladen",
}, "grasscutter_stable_update": "Stabile Grasscutter Version aktualisieren",
"download_status": { "grasscutter_latest_update": "Aktuellste Grasscutter Version aktualisieren",
"downloading": "Lädt herunter", "resources": "Grasscutter Ressourcen herunterladen",
"extracting": "Extrahiert", "game": "Spiel herunterladen"
"error": "Fehler", },
"finished": "Fertig", "download_status": {
"stopped": "Gestoppt" "downloading": "Lädt herunter",
}, "extracting": "Extrahiert",
"components": { "error": "Fehler",
"select_file": "Datei oder Ordner auswählen...", "finished": "Fertig",
"select_folder": "Ordner auswählen...", "stopped": "Gestoppt"
"download": "Herunterladen" },
}, "components": {
"news": { "select_file": "Datei oder Ordner auswählen...",
"latest_commits": "Letzte Commits", "select_folder": "Ordner auswählen...",
"latest_version": "Letzte Version" "download": "Herunterladen",
}, "install": "Installieren"
"help": { },
"port_help_text": "Vergewissern Sie sich, dass es sich um den Port des Dispatch-Servers handelt, nicht um den Port des Spiel-Servers. Dieser ist fast immer '443'.", "news": {
"game_help_text": "Sie müssen keine separate Kopie verwenden, um mit Grasscutter zu spielen. Dies ist entweder für ein Downgrade auf die Version 2.6 oder wenn Sie das Spiel nicht installiert haben.", "latest_commits": "Letzte Commits",
"gc_stable_jar": "Laden Sie den aktuellen stabilen Grasscutter-Build herunter, der eine Jar-Datei und Datendateien enthält.", "latest_version": "Letzte Version"
"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.", "help": {
"gc_dev_data": "Laden Sie die neuesten Grasscutter-Entwicklungsdateien herunter, welche keine Jar-Datei enthält. Dies ist nützlich zum Aktualisieren.", "port_help_text": "Vergewissern Sie sich, dass es sich um den Port des Dispatch-Servers handelt, nicht um den Port des Spiel-Servers. Dieser ist fast immer '443'.",
"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" "game_help_text": "Sie müssen keine separate Kopie verwenden, um mit Grasscutter zu spielen. Dies ist entweder für ein Downgrade auf die Version 2.6 oder wenn Sie das Spiel nicht installiert haben.",
} "gc_stable_jar": "Laden Sie den aktuellen stabilen Grasscutter-Build herunter, der eine Jar-Datei und Datendateien enthält.",
} "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",
"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"
}
}

View File

@@ -23,7 +23,9 @@
"grasscutter_with_game": "Automatically launch Grasscutter with game", "grasscutter_with_game": "Automatically launch Grasscutter with game",
"language": "Select Language", "language": "Select Language",
"background": "Set Custom Background (link or image file)", "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": { "downloads": {
"grasscutter_stable_data": "Download Grasscutter Stable Data", "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_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_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.", "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": { "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
View 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"
}
}

View File

@@ -18,7 +18,7 @@
"background": "Atur Kustom Latar Belakang (link atau gambar file)", "background": "Atur Kustom Latar Belakang (link atau gambar file)",
"theme": "Atur Tema" "theme": "Atur Tema"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "Sedang Mendownload Grasscutter Versi Stabil", "grasscutter_stable_data": "Sedang Mendownload Grasscutter Versi Stabil",
"grasscutter_latest_data": "Sedang Mendownload Grasscutter Data Terbaru", "grasscutter_latest_data": "Sedang Mendownload Grasscutter Data Terbaru",
"grasscutter_stable_data_update": "Memperbaharui Grasscutter Data Stabil", "grasscutter_stable_data_update": "Memperbaharui Grasscutter Data Stabil",
@@ -54,4 +54,4 @@
"gc_dev_data": "Unduh file data Grasscutter Development saat ini, dimana Tidak Ada JAR file. Ini Berguna Untuk memperbarui.", "gc_dev_data": "Unduh file data Grasscutter Development saat ini, dimana Tidak Ada JAR file. Ini Berguna Untuk memperbarui.",
"resources": "Ini juga diperlukan untuk menjalankan server Grasscutter. Tombol ini akan berwarna abu-abu jika Anda memiliki folder Resource yang ada dengan File di dalamnya" "resources": "Ini juga diperlukan untuk menjalankan server Grasscutter. Tombol ini akan berwarna abu-abu jika Anda memiliki folder Resource yang ada dengan File di dalamnya"
} }
} }

View File

@@ -1,61 +1,80 @@
{ {
"lang_name": "Русский", "lang_name": "Русский",
"main": { "main": {
"title": "Cultivation", "title": "Cultivation",
"launch_button": "Запустить", "launch_button": "Запустить",
"gc_enable": "Подключиться с Grasscutter", "gc_enable": "Подключиться с Grasscutter",
"https_enable": "Исп. HTTPS", "https_enable": "Исп. HTTPS",
"ip_placeholder": "Айпи адрес...", "ip_placeholder": "Айпи адрес...",
"port_placeholder": "Порт...", "port_placeholder": "Порт...",
"files_downloading": "Файлов скачано: ", "files_downloading": "Файлов скачано: ",
"files_extracting": "Извлечено файлов: " "files_extracting": "Извлечено файлов: "
}, },
"options": { "options": {
"enabled": "Включено", "enabled": "Включено",
"disabled": "Выключено", "disabled": "Выключено",
"game_executable": "Установить исполняемый файл игры", "game_path": "Установить путь к файлам игры",
"grasscutter_jar": "Установить Grasscutter JAR", "game_executable": "Установить исполняемый файл игры",
"toggle_encryption": "Переключить шифрование", "recover_metadata": "Принудительное восстановление Метаданных",
"java_path": "Установить пользовательский путь Java", "grasscutter_jar": "Установить Grasscutter JAR",
"grasscutter_with_game": "Автоматически запускать Grasscutter вместе с игрой", "toggle_encryption": "Переключить шифрование",
"language": "Установить язык", "install_certificate": "Установить сертификат для работы Прокси",
"background": "Установить свой фон (ссылка или файл)", "java_path": "Установить пользовательский путь Java",
"theme": "Установить тему" "grasscutter_with_game": "Автоматически запускать Grasscutter вместе с игрой",
}, "language": "Установить язык",
"downloads": { "background": "Установить свой фон (ссылка или файл)",
"grasscutter_stable_data": "Скачать стабильные данные Grasscutter", "theme": "Установить тему",
"grasscutter_latest_data": "Скачать последние данные Grasscutter", "patch_metadata": "Автоматический патч Метаданных при запуске",
"grasscutter_stable_data_update": "Обновить стабильные данные Grasscutter", "use_proxy": "Использовать встроенный Прокси"
"grasscutter_latest_data_update": "Обновить последние данные Grasscutter", },
"grasscutter_stable": "Скачать стабильную версию Grasscutter", "downloads": {
"grasscutter_latest": "Скачать последнюю версию Grasscutter", "grasscutter_stable_data": "Скачать стабильные данные Grasscutter",
"grasscutter_stable_update": "Обновить стабильную версию Grasscutter", "grasscutter_latest_data": "Скачать последние данные Grasscutter",
"grasscutter_latest_update": "Обновить последнюю версию Grasscutter", "grasscutter_stable_data_update": "Обновить стабильные данные Grasscutter",
"resources": "Скачать ресурсы Grasscutter" "grasscutter_latest_data_update": "Обновить последние данные Grasscutter",
}, "grasscutter_stable": "Скачать стабильную версию Grasscutter",
"download_status": { "grasscutter_latest": "Скачать последнюю версию Grasscutter",
"downloading": "Скачивание", "grasscutter_stable_update": "Обновить стабильную версию Grasscutter",
"extracting": "Извлечение", "grasscutter_latest_update": "Обновить последнюю версию Grasscutter",
"error": "Ошибка", "resources": "Скачать ресурсы Grasscutter",
"finished": "Закончено", "game": "Скачать Игру"
"stopped": "Остановлено" },
}, "download_status": {
"components": { "downloading": "Скачивание",
"select_file": "Выберите файл или папку...", "extracting": "Извлечение",
"select_folder": "Выберите папку...", "error": "Ошибка",
"download": "Скачать" "finished": "Закончено",
}, "stopped": "Остановлено"
"news": { },
"latest_commits": "Последние коммиты", "components": {
"latest_version": "Последняя версия" "select_file": "Выберите файл или папку...",
}, "select_folder": "Выберите папку...",
"help": { "download": "Скачать",
"port_help_text": "Убедитесь, что это порт Dispatch-сервера, не порт игрового сервера. Обычно это '443'.", "install": "Установить"
"game_help_text": "Вам не нужно устанавливать еще одну копию, что бы играть с Grascutter. Это нужно или для версии 2.6, или если у Вас не установлена игра.", },
"gc_stable_jar": "Скачать последнюю стабильную версию Grasscutter, которая содержит jar файл и данные.", "news": {
"gc_dev_jar": "Скачать последнюю версию для разработки Grasscutter, которая содержит jar файл и данные.", "latest_commits": "Последние коммиты",
"gc_stable_data": "Скачать стабильные данные Grasscutter, в которой нету jar файла. Это полезно для обновления.", "latest_version": "Последняя версия"
"gc_dev_data": "Скачать последнюю версию для разработки Grasscutter, в которой нету jar файла. Это полезно для обновления.", },
"resources": "Это необходимо для запуска сервера Grasscutter. Эта кнопка будет серой, если у Вас уже есть не пустая папка с ресурсами." "help": {
} "port_help_text": "Убедитесь, что это порт Dispatch-сервера, не порт игрового сервера. Обычно это '443'.",
} "game_help_text": "Вам не нужно устанавливать еще одну копию, что бы играть с Grascutter. Это нужно или для версии 2.6, или если у Вас не установлена игра.",
"gc_stable_jar": "Скачать последнюю стабильную версию Grasscutter, которая содержит jar файл и данные.",
"gc_dev_jar": "Скачать последнюю версию для разработки Grasscutter, которая содержит jar файл и данные.",
"gc_stable_data": "Скачать стабильные данные Grasscutter, в которой нету jar файла. Это полезно для обновления.",
"gc_dev_data": "Скачать последнюю версию для разработки Grasscutter, в которой нету jar файла. Это полезно для обновления.",
"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"
}
}

View File

@@ -3,59 +3,70 @@
"main": { "main": {
"title": "Cultivation", "title": "Cultivation",
"launch_button": "Khởi Chạy", "launch_button": "Khởi Chạy",
"gc_enable": "Kết nối đến Grasscutter", "gc_enable": "Kết nối qua Grasscutter",
"https_enable": "Sử dụng HTTPS", "https_enable": "ng HTTPS",
"ip_placeholder": "Địa chỉ máy chủ...", "ip_placeholder": "Địa chỉ máy chủ...",
"port_placeholder": "Cổng...", "port_placeholder": "Cổng...",
"files_downloading": "Đang tải file: ", "files_downloading": "Đang tải tập tin: ",
"files_extracting": "Đang giải nén tp tin: " "files_extracting": "Đang giải nén tp tin: "
}, },
"options": { "options": {
"enabled": "Bật", "enabled": "Bật",
"disabled": "Tắt", "disabled": "Tắt",
"game_exec": "Đường dẫn đến GenshinImpact.exe", "game_path": "Đường dẫn cài game",
"grasscutter_jar": "Đường dẫn đến Grasscutter.jar", "game_executable": "Tập tin thực thi game",
"toggle_encryption": "Bật/Tắt mã hoá", "recover_metadata": "Khôi phục Metadata khẩn cấp",
"java_path": "Đường dẫn Java tuỳ chỉnh", "grasscutter_jar": "Tập tin JAR Grasscutter",
"grasscutter_with_game": "Tự động khởi chạy Grasscutter cùng game", "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ữ", "language": "Chọn ngôn ngữ",
"background": "nh nền tuỳ chỉnh (đường dẫn hoặc tp tin ảnh)", "background": "nh nền tùy chỉnh (liên kết hoặc tp tin hình ảnh)",
"theme": "Chọn giao diện" "theme": "Giao diện",
"patch_metadata": "Tự động sửa Metadata",
"use_proxy": "Sử dụng proxy nội bộ"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "Tải xuống dữ liệu Grasscutter bản ổn định", "grasscutter_stable_data": "Tải 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_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_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_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_stable": "Tải Grasscutter bản ổn định",
"grasscutter_latest": "Tải xuống Grasscutter phiển bản mới nhất", "grasscutter_latest": "Tải Grasscutter bản mới nhất",
"grasscutter_stable_update": "Cập nhật Grasscutter ổn định", "grasscutter_stable_update": "Cập nhật Grasscutter bản ổn định",
"grasscutter_latest_update": "Cập nhật Grasscutter mới nhất", "grasscutter_latest_update": "Cập nhật Grasscutter bản mới nhất",
"resources": "Tải xuống tài nguyên cho Grasscutter" "resources": "Tải tài nguyên Grasscutter",
"game": "Tải game"
}, },
"download_status": { "download_status": {
"downloading": "Đang tải", "downloading": "Đang tải xuống",
"extracting": "Đang giải nén", "extracting": "Đang giải nén",
"error": "Lỗi", "error": "Lỗi",
"finished": "Đã xong", "finished": "Hoàn thành",
"stopped": "Đã dừng" "stopped": "Đã dừng"
}, },
"components": { "components": {
"select_file": "Chọn tp tin hoặc thư mục...", "select_file": "Chọn tp tin hoặc thư mục...",
"select_folder": "Chọn 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": { "news": {
"latest_commits": "Cập nhật gần đây", "latest_commits": "Thay Đổi Gần Đây",
"latest_version": "Phiên bản mới nhất" "latest_version": "Phiên Bản Mới Nhất"
}, },
"help": { "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'.", "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 đặt trò chơi.", "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, bo gồm file jar và các file dữ liệu.", "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, bo 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, bao gồm tập tin jar và các tệp 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_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 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.", "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 một thư mục tài nguyên có nội dung bên trong" "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.

View File

@@ -43,7 +43,7 @@ bool gen_global_metadata_key(uint8_t* src, size_t srcn) {
uint64_t* key = (uint64_t*)src; 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(); key[i] = rand();
*(uint16_t *) (src + 0xc8) = 0xfc2e; // Magic *(uint16_t *) (src + 0xc8) = 0xfc2e; // Magic
@@ -53,7 +53,7 @@ bool gen_global_metadata_key(uint8_t* src, size_t srcn) {
return true; 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 longkey[0xB00];
uint8_t longkeyp[0xB0]; uint8_t longkeyp[0xB0];
uint8_t shortkey[16]; 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); 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]; uint8_t literal_dec_key[0x5000];
gen_global_metadata_key(data + size - 0x4000, 0x4000); 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;
}
}

View File

@@ -4,7 +4,7 @@
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
extern "C" void decrypt_global_metadata(uint8_t *data, size_t size); extern "C" int decrypt_global_metadata(uint8_t *data, size_t size);
extern "C" void encrypt_global_metadata(uint8_t *data, size_t size); extern "C" int encrypt_global_metadata(uint8_t *data, size_t size);
#endif //METADATA_H #endif //METADATA_H

View File

@@ -1,3 +1,4 @@
newline_style = "Unix"
tab_spaces = 2 tab_spaces = 2
use_field_init_shorthand = true use_field_init_shorthand = true
use_try_shorthand = true use_try_shorthand = true

View File

@@ -1,6 +1,6 @@
use std::fs;
use file_diff::diff; use file_diff::diff;
use std::{io::{Read, Write}}; use std::fs;
use std::io::{Read, Write};
#[tauri::command] #[tauri::command]
pub fn rename(path: String, new_name: String) { 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); 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] #[tauri::command]
@@ -133,19 +145,11 @@ pub fn write_file(path: String, contents: String) {
let path_buf = std::path::PathBuf::from(&path); let path_buf = std::path::PathBuf::from(&path);
// Create file if it exists, otherwise just open and rewrite // 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, Ok(file) => file,
Err(e) => { Err(e) => {
println!("Failed to open file: {}", e); println!("Failed to open file: {}", e);
return;
// attempt to create file. otherwise return
match fs::File::create(&path_buf) {
Ok(file) => file,
Err(e) => {
println!("Failed to create file: {}", e);
return;
}
}
} }
}; };
@@ -154,7 +158,6 @@ pub fn write_file(path: String, contents: String) {
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
println!("Failed to write to file: {}", e); println!("Failed to write to file: {}", e);
return;
} }
} }
} }

View 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
}

View File

@@ -12,17 +12,23 @@ use sysinfo::{System, SystemExt};
mod downloader; mod downloader;
mod file_helpers; mod file_helpers;
mod gamebanana;
mod lang; mod lang;
mod metadata_patcher;
mod proxy; mod proxy;
mod structs; mod structs;
mod system_helpers; mod system_helpers;
mod unzip; mod unzip;
mod web; mod web;
mod metadata_patcher;
static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new())); static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
fn main() { 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() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
enable_process_watcher, enable_process_watcher,
@@ -34,6 +40,7 @@ fn main() {
get_theme_list, get_theme_list,
system_helpers::run_command, system_helpers::run_command,
system_helpers::run_program, system_helpers::run_program,
system_helpers::run_program_relative,
system_helpers::run_jar, system_helpers::run_jar,
system_helpers::open_in_browser, system_helpers::open_in_browser,
system_helpers::install_location, system_helpers::install_location,
@@ -42,6 +49,7 @@ fn main() {
proxy::generate_ca_files, proxy::generate_ca_files,
unzip::unzip, unzip::unzip,
file_helpers::rename, file_helpers::rename,
file_helpers::dir_create,
file_helpers::dir_exists, file_helpers::dir_exists,
file_helpers::dir_is_empty, file_helpers::dir_is_empty,
file_helpers::dir_delete, file_helpers::dir_delete,
@@ -57,6 +65,9 @@ fn main() {
lang::get_languages, lang::get_languages,
web::valid_url, web::valid_url,
web::web_get, web::web_get,
gamebanana::get_download_links,
gamebanana::list_submissions,
gamebanana::list_mods,
metadata_patcher::patch_metadata metadata_patcher::patch_metadata
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
@@ -72,7 +83,7 @@ fn is_game_running() -> bool {
} }
#[tauri::command] #[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; *WATCH_GAME_PROCESS.lock().unwrap() = process;
window.listen("disable_process_watcher", |_e| { window.listen("disable_process_watcher", |_e| {
@@ -82,6 +93,9 @@ fn enable_process_watcher(window: tauri::Window,process: String) {
println!("Starting process watcher..."); println!("Starting process watcher...");
thread::spawn(move || { 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(); let mut system = System::new_all();
loop { loop {

View File

@@ -4,23 +4,10 @@ use std::fs::OpenOptions;
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
extern { // For these two functions, a non-zero return value indicates failure.
fn decrypt_global_metadata(data : *mut u8, size : u64); extern "C" {
fn encrypt_global_metadata(data : *mut u8, size : u64); fn decrypt_global_metadata(data: *mut u8, size: usize) -> i32;
} fn encrypt_global_metadata(data: *mut u8, size: usize) -> i32;
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)
}
} }
#[tauri::command] #[tauri::command]
@@ -46,7 +33,11 @@ pub fn patch_metadata(metadata_folder: &str) -> bool {
} }
//write encrypted to file //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, Ok(file) => file,
Err(e) => { Err(e) => {
println!("Failed to open global-metadata: {}", e); println!("Failed to open global-metadata: {}", e);
@@ -55,7 +46,7 @@ pub fn patch_metadata(metadata_folder: &str) -> bool {
}; };
file.write_all(&encrypted).unwrap(); file.write_all(&encrypted).unwrap();
true true
} }
@@ -79,18 +70,14 @@ fn decrypt_metadata(file_path: &str) -> Vec<u8> {
} }
// Decrypt metadata file // Decrypt metadata file
match dll_decrypt_global_metadata(data.as_mut_ptr(), data.len().try_into().unwrap()) { let success = unsafe { decrypt_global_metadata(data.as_mut_ptr(), data.len()) } == 0;
Ok(_) => { if success {
println!("Successfully decrypted global-metadata"); println!("Successfully decrypted global-metadata");
data data
} } else {
Err(e) => { println!("Failed to decrypt global-metadata");
println!("Failed to decrypt global-metadata: {}", e); Vec::new()
Vec::new() }
}
};
Vec::new()
} }
fn replace_keys(data: &[u8]) -> Vec<u8> { 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 { fn replace_rsa_key(old_data: &str, to_replace: &str, file_name: &str) -> String {
// Read dispatch key file // Read dispatch key file
unsafe { 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, Ok(file) => file,
Err(e) => { Err(e) => {
println!("Failed to open keys/{}: {}", file_name, 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> { fn encrypt_metadata(old_data: &[u8]) -> Vec<u8> {
let mut data = old_data.to_vec(); let mut data = old_data.to_vec();
match dll_encrypt_global_metadata(data.as_mut_ptr(), data.len().try_into().unwrap()) { let success = unsafe { encrypt_global_metadata(data.as_mut_ptr(), data.len()) } == 0;
Ok(_) => { if success {
println!("Successfully encrypted global-metadata"); println!("Successfully encrypted global-metadata");
data data
} } else {
Err(e) => { println!("Failed to encrypt global-metadata");
println!("Failed to encrypt global-metadata: {}", e); Vec::new()
Vec::new() }
}
};
Vec::new()
} }
fn do_vecs_match<T: PartialEq>(a: &Vec<T>, b: &Vec<T>) -> bool { 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(); a == b
matching == a.len() && matching == b.len() }
}

View File

@@ -242,6 +242,7 @@ pub fn install_ca_files(cert_path: &Path) {
crate::system_helpers::run_command( crate::system_helpers::run_command(
"certutil", "certutil",
vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()], vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()],
None,
); );
println!("Installed certificate."); println!("Installed certificate.");
} }

View File

@@ -1,17 +1,54 @@
use duct::cmd; use duct::cmd;
#[tauri::command] #[tauri::command]
pub fn run_program(path: String) { pub fn run_program(path: String, args: Option<String>) {
// Open in new thread to prevent blocking. // Without unwrap_or, this can crash when UAC prompt is denied
std::thread::spawn(move || { open::that(format!("{} {}", &path, &args.unwrap_or("".into()))).unwrap_or(());
// Without unwrap_or, this can crash when UAC prompt is denied
open::that(&path).unwrap_or(());
});
} }
#[tauri::command] #[tauri::command]
pub fn run_command(program: &str, args: Vec<&str>) { pub fn run_program_relative(path: String, args: Option<String>) {
cmd(program, args).run().expect("Failed to run command"); // 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] #[tauri::command]

View File

@@ -1,9 +1,16 @@
use std::fs::File; use std::fs::{read_dir, File};
use std::path; use std::path;
use std::thread; use std::thread;
use unrar::archive::Archive;
#[tauri::command] #[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 // Read file TODO: replace test file
let f = match File::open(&zipfile) { let f = match File::open(&zipfile) {
Ok(f) => f, Ok(f) => f,
@@ -15,40 +22,80 @@ pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
let write_path = path::PathBuf::from(&destpath); 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 // Run extraction in seperate thread
thread::spawn(move || { thread::spawn(move || {
let full_path = write_path; let mut full_path = write_path.clone();
window.emit("extract_start", &zipfile).unwrap(); window.emit("extract_start", &zipfile).unwrap();
match zip_extract::extract(&f, &full_path, true) { if folder_if_loose.unwrap_or(false) {
Ok(_) => { // Create a new folder with the same name as the zip file
println!( let mut file_name = path::Path::new(&zipfile)
"Extracted zip file to: {}", .file_name()
full_path.to_str().unwrap_or("Error") .unwrap()
); .to_str()
} .unwrap();
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()); // remove ".zip" from the end of the file name
file_name = &file_name[..file_name.len() - 4];
res_hash.insert("path".to_string(), zipfile.to_string()); let new_path = full_path.join(file_name);
match std::fs::create_dir_all(&new_path) {
Ok(_) => {}
Err(e) => {
println!("Failed to create directory: {}", e);
return;
}
};
window.emit("download_error", &res_hash).unwrap(); full_path = new_path.clone();
} }
};
// Get the name of the inenr file in the zip file println!("Is rar file? {}", zipfile.ends_with(".rar"));
let mut zip = zip::ZipArchive::new(&f).unwrap();
let file = zip.by_index(0).unwrap(); let name;
let name = file.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();
name = file.name().to_string().clone();
}
// If the contents is a jar file, emit that we have extracted a new jar file // If the contents is a jar file, emit that we have extracted a new jar file
if name.ends_with(".jar") { if name.ends_with(".jar") {
window window
.emit("jar_extracted", destpath.to_string() + name) .emit("jar_extracted", destpath.to_string() + name.as_str())
.unwrap(); .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();
}
};
}

View File

@@ -1,4 +1,4 @@
use reqwest::header::USER_AGENT; use reqwest::header::{CONTENT_TYPE, USER_AGENT};
pub(crate) async fn query(site: &str) -> String { pub(crate) async fn query(site: &str) -> String {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
@@ -6,6 +6,7 @@ pub(crate) async fn query(site: &str) -> String {
let response = client let response = client
.get(site) .get(site)
.header(USER_AGENT, "cultivation") .header(USER_AGENT, "cultivation")
.header(CONTENT_TYPE, "application/json")
.send() .send()
.await .await
.unwrap(); .unwrap();

View File

@@ -7,25 +7,17 @@
}, },
"package": { "package": {
"productName": "Cultivation", "productName": "Cultivation",
"version": "1.0.3" "version": "1.0.6"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
"fs": { "fs": {
"scope": [ "scope": ["$DATA", "$DATA/cultivation", "$DATA/cultivation/*"]
"$DATA",
"$DATA/cultivation",
"$DATA/cultivation/*"
]
}, },
"protocol": { "protocol": {
"all": true, "all": true,
"asset": true, "asset": true,
"assetScope": [ "assetScope": ["$DATA", "$DATA/cultivation", "$DATA/cultivation/*"]
"$DATA",
"$DATA/cultivation",
"$DATA/cultivation/*"
]
}, },
"all": true "all": true
}, },
@@ -37,13 +29,7 @@
"depends": [] "depends": []
}, },
"externalBin": [], "externalBin": [],
"icon": [ "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "io.grasscutter", "identifier": "io.grasscutter",
"shortDescription": "A game launcher.", "shortDescription": "A game launcher.",
"longDescription": "A launcher for a certain anime game that proxies all related game traffic to external servers.", "longDescription": "A launcher for a certain anime game that proxies all related game traffic to external servers.",
@@ -54,11 +40,7 @@
"providerShortName": null, "providerShortName": null,
"signingIdentity": null "signingIdentity": null
}, },
"resources": [ "resources": ["lang/*.json", "keys/*"],
"lang/*.json",
"keys/*",
"./mhycrypto.dll"
],
"targets": "all", "targets": "all",
"windows": { "windows": {
"allowDowngrades": false, "allowDowngrades": false,
@@ -93,4 +75,4 @@
} }
] ]
} }
} }

View File

@@ -1,13 +1,11 @@
body { body {
margin: 0; margin: 0;
font-family: 'MiHoYo_SDK_Web', 'Helvetica Neue', BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: 'MiHoYo_SDK_Web', 'Helvetica Neue', BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Cantarell', 'Fira Sans', 'Droid Sans', sans-serif;
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
monospace;
} }

View File

@@ -7,23 +7,15 @@ import Debug from './ui/Debug'
import { getConfigOption } from './utils/configuration' import { getConfigOption } from './utils/configuration'
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
document.getElementById('root') as HTMLElement
)
let isDebug = false; let isDebug = false
(async() => { ;async () => {
isDebug = await getConfigOption('debug_enabled') isDebug = await getConfigOption('debug_enabled')
}) }
root.render( root.render(<React.StrictMode>{isDebug ? <Debug /> : <App />}</React.StrictMode>)
<React.StrictMode>
{
isDebug ? <Debug /> : <App />
}
</React.StrictMode>
)
import reportWebVitals from './utils/reportWebVitals' import reportWebVitals from './utils/reportWebVitals'
isDebug && reportWebVitals(console.log) isDebug && reportWebVitals(console.log)

View 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

View 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

View 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

View 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

View 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

View File

@@ -22,7 +22,8 @@ select:focus {
border-bottom-color: #ffd326; border-bottom-color: #ffd326;
} }
#root, .App { #root,
.App {
height: 100%; height: 100%;
} }
@@ -34,12 +35,29 @@ select:focus {
.TopButton { .TopButton {
height: 60%; height: 60%;
margin: 0px 10px; 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%); filter: invert(95%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(88%) contrast(81%);
transition: filter 0.1s ease-in-out; transition: filter 0.1s ease-in-out;
} }
.TopButton:hover { .TopButton:hover {
color: #fff;
cursor: pointer;
}
.TopButton:hover img {
filter: invert(100%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(100%) contrast(100%); filter: invert(100%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(100%) contrast(100%);
cursor: pointer; cursor: pointer;
} }
@@ -64,8 +82,8 @@ select:focus {
} }
.arrow-down { .arrow-down {
width: 0; width: 0;
height: 0; height: 0;
border-left: 50px solid transparent; border-left: 50px solid transparent;
border-right: 50px solid transparent; border-right: 50px solid transparent;
border-top: 50px solid transparent; border-top: 50px solid transparent;
@@ -82,28 +100,28 @@ select:focus {
.BottomSection { .BottomSection {
position: absolute; position: absolute;
bottom: 0%; bottom: 0%;
left: 50%; left: 50%;
transform: translate(-50%, 0%); transform: translate(-50%, 0%);
width: 100%; width: 100%;
height: 160px; height: 160px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
box-shadow: inset 0px 5px 12px -3px rgb(50 50 50 / 75%); box-shadow: inset 0px 5px 12px -3px rgb(50 50 50 / 75%);
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@media(max-height: 580px) { @media (max-height: 580px) {
.BottomSection { .BottomSection {
height: 150px; height: 150px;
} }
} }
@media(max-height: 500px) { @media (max-height: 500px) {
.BottomSection { .BottomSection {
height: 140px; height: 140px;
} }
} }

View File

@@ -1,95 +1,33 @@
import React from 'react' import React from 'react'
import { listen } from '@tauri-apps/api/event'
import './App.css' import './App.css'
import DownloadHandler from '../utils/download' import DownloadHandler from '../utils/download'
import { getConfigOption } from '../utils/configuration'
// 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 { getTheme, loadTheme } from '../utils/themes' import { getTheme, loadTheme } from '../utils/themes'
import { unpatchGame } from '../utils/metadata' import { convertFileSrc, invoke } from '@tauri-apps/api/tauri'
import { dataDir } from '@tauri-apps/api/path'
interface IProps { import { Main } from './Main'
[key: string]: never; import { Mods } from './Mods'
}
interface IState { interface IState {
isDownloading: boolean; page: string
optionsOpen: boolean; bgFile: string
miniDownloadsOpen: boolean;
downloadsOpen: boolean;
gameDownloadsOpen: boolean;
bgFile: string;
} }
const downloadHandler = new DownloadHandler()
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile' const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
const downloadHandler = new DownloadHandler() class App extends React.Component<Readonly<unknown>, IState> {
constructor(props: Readonly<unknown>) {
class App extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props) super(props)
this.state = { this.state = {
isDownloading: false, page: 'main',
optionsOpen: false,
miniDownloadsOpen: false,
downloadsOpen: false,
gameDownloadsOpen: false,
bgFile: DEFAULT_BG, 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() { async componentDidMount() {
const cert_generated = await getConfigOption('cert_generated')
const game_exe = await getConfigOption('game_install_path') const game_exe = await getConfigOption('game_install_path')
const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || '' const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || ''
const root_path = game_path?.substring(0, game_path.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 !! // Get custom bg AFTER theme is loaded !! important !!
const custom_bg = await getConfigOption('customBackground') const custom_bg = await getConfigOption('customBackground')
if(!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) { if (!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) {
if(game_path) { if (game_path) {
// Get the bg by invoking, then set the background to that bg. // Get the bg by invoking, then set the background to that bg.
const bgLoc: string = await invoke('get_bg_file', { const bgLoc: string = await invoke('get_bg_file', {
bgPath: root_path, bgPath: root_path,
appdata: await dataDir() appdata: await dataDir(),
}) })
bgLoc && this.setState({ bgLoc &&
bgFile: bgLoc this.setState(
}, this.forceUpdate) {
bgFile: bgLoc,
},
this.forceUpdate
)
} }
} else { } else {
const isUrl = /^http(s)?:\/\//gm.test(custom_bg) const isUrl = /^http(s)?:\/\//gm.test(custom_bg)
if (!isUrl) { if (!isUrl) {
const isValid = await invoke('dir_exists', { const isValid = await invoke('dir_exists', {
path: custom_bg path: custom_bg,
}) })
this.setState({ this.setState(
bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG {
}, this.forceUpdate) bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG,
},
this.forceUpdate
)
} else { } else {
// Check if URL returns a valid image. // Check if URL returns a valid image.
const isValid = await invoke('valid_url', { const isValid = await invoke('valid_url', {
url: custom_bg url: custom_bg,
}) })
this.setState({ this.setState(
bgFile: isValid ? custom_bg : DEFAULT_BG {
}, this.forceUpdate) bgFile: isValid ? custom_bg : DEFAULT_BG,
},
this.forceUpdate
)
} }
} }
if (!cert_generated) { window.addEventListener('changePage', (e) => {
// 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({ this.setState({
isDownloading: downloadHandler.getDownloads().filter(d => d.status !== 'finished')?.length > 0 // @ts-expect-error - TS doesn't like our custom event
page: e.detail,
}) })
}, 1000) })
} }
render() { render() {
return ( return (
<div className="App" style={ <div
this.state.bgFile ? { className="App"
background: `url("${this.state.bgFile}") fixed`, style={
} : {} this.state.bgFile
}> ? {
<TopBar background: `url("${this.state.bgFile}") fixed`,
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
} }
>
{ {(() => {
// Download menu switch (this.state.page) {
this.state.downloadsOpen ? ( case 'modding':
<Downloads return <Mods downloadHandler={downloadHandler} />
downloadManager={downloadHandler} default:
closeFn={() => this.setState({ downloadsOpen: false })} return <Main downloadHandler={downloadHandler} />
/> }
) : 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> </div>
) )
} }

View File

@@ -3,8 +3,8 @@ import './App.css'
import TopBar from './components/TopBar' import TopBar from './components/TopBar'
import {invoke} from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
import {dataDir} from '@tauri-apps/api/path' import { dataDir } from '@tauri-apps/api/path'
import TextInput from './components/common/TextInput' import TextInput from './components/common/TextInput'
let proxyAddress = '' let proxyAddress = ''
@@ -15,7 +15,7 @@ async function setProxyAddress(address: string) {
} }
async function startProxy() { 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' }) await invoke('open_in_browser', { url: 'https://hoyoverse.com' })
} }
@@ -24,27 +24,23 @@ async function stopProxy() {
} }
async function generateCertificates() { async function generateCertificates() {
await invoke('generate_ca_files', { path: await dataDir() + '\\cultivation' }) await invoke('generate_ca_files', { path: (await dataDir()) + '\\cultivation' })
} }
async function generateInfo() { async function generateInfo() {
console.log({ console.log({
certificatePath: await dataDir() + '\\cultivation\\ca', certificatePath: (await dataDir()) + '\\cultivation\\ca',
isAdmin: await invoke('is_elevated'), isAdmin: await invoke('is_elevated'),
connectingTo: proxyAddress connectingTo: proxyAddress,
}) })
alert('check your dev console and send that in #cultivation') alert('check your dev console and send that in #cultivation')
} }
function none() { class Debug extends React.Component {
alert('none')
}
class Debug extends React.Component{
render() { render() {
return ( return (
<div className="App"> <div className="App">
<TopBar optFunc={none} downFunc={none} gameFunc={none} /> <TopBar />
<TextInput readOnly={false} initalValue={'change to set proxy address'} onChange={setProxyAddress} /> <TextInput readOnly={false} initalValue={'change to set proxy address'} onChange={setProxyAddress} />
<button onClick={startProxy}>start proxy</button> <button onClick={startProxy}>start proxy</button>
<button onClick={stopProxy}>stop proxy</button> <button onClick={stopProxy}>stop proxy</button>
@@ -55,4 +51,4 @@ class Debug extends React.Component{
} }
} }
export default Debug export default Debug

241
src/ui/Main.tsx Normal file
View 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
View 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
View 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>
)
}
}

View File

@@ -1,7 +1,7 @@
.MiniDialog { .MiniDialog {
position: fixed; position: fixed;
z-index: 99; z-index: 99;
/* Len and width */ /* Len and width */
height: 30%; height: 30%;
width: 30%; width: 30%;
@@ -32,4 +32,4 @@
.MiniDialog .ProgressText { .MiniDialog .ProgressText {
color: #000; color: #000;
} }

View File

@@ -4,10 +4,10 @@ import Close from '../../resources/icons/close.svg'
import './MiniDialog.css' import './MiniDialog.css'
interface IProps { interface IProps {
children: React.ReactNode[] | React.ReactNode; children: React.ReactNode[] | React.ReactNode
title?: string; title?: string
closeable?: boolean; closeable?: boolean
closeFn: () => void; closeFn: () => void
} }
export default class MiniDialog extends React.Component<IProps, never> { export default class MiniDialog extends React.Component<IProps, never> {
@@ -19,7 +19,7 @@ export default class MiniDialog extends React.Component<IProps, never> {
document.addEventListener('mousedown', (evt) => { document.addEventListener('mousedown', (evt) => {
const tgt = evt.target as HTMLElement const tgt = evt.target as HTMLElement
const isInside = tgt.closest('.MiniDialog') !== null const isInside = tgt.closest('.MiniDialog') !== null
if (!isInside) { if (!isInside) {
this.props.closeFn() this.props.closeFn()
} }
@@ -33,13 +33,12 @@ export default class MiniDialog extends React.Component<IProps, never> {
render() { render() {
return ( return (
<div className="MiniDialog" id="miniDialogContainer"> <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}>
<div className="MiniDialogTop" id="miniDialogContainerTop" onClick={this.props.closeFn}> <span>{this.props?.title}</span>
<span>{this.props?.title}</span> <img src={Close} className="MiniDialogClose" id="miniDialogButtonClose" />
<img src={Close} className="MiniDialogClose" id="miniDialogButtonClose" /> </div>
</div> : null ) : null}
}
<div className="MiniDialogInner" id="miniDialogContent"> <div className="MiniDialogInner" id="miniDialogContent">
{this.props.children} {this.props.children}
@@ -47,4 +46,4 @@ export default class MiniDialog extends React.Component<IProps, never> {
</div> </div>
) )
} }
} }

View File

@@ -2,7 +2,7 @@
position: absolute; position: absolute;
transform: translate(0%, 0%); transform: translate(0%, 0%);
display:flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
@@ -36,14 +36,14 @@
filter: invert(75%) sepia(0%) saturate(100%) hue-rotate(0deg) brightness(100%) contrast(100%); filter: invert(75%) sepia(0%) saturate(100%) hue-rotate(0deg) brightness(100%) contrast(100%);
} }
@media(max-height: 580px) { @media (max-height: 580px) {
.RightBar { .RightBar {
height: calc(100vh - 180px); height: calc(100vh - 180px);
} }
} }
@media(max-height: 500px) { @media (max-height: 500px) {
.RightBar { .RightBar {
height: calc(100vh - 170px); height: calc(100vh - 170px);
} }
} }

View File

@@ -1,8 +1,8 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api'
import React from 'react' import React from 'react'
import Discord from '../../resources/icons/discord.svg' import Discord from '../../resources/icons/discord.svg'
import Github from '../../resources/icons/github.svg' import Github from '../../resources/icons/github.svg'
import './RightBar.css' import './RightBar.css'
@@ -28,4 +28,4 @@ export default class RightBar extends React.Component {
</div> </div>
) )
} }
} }

View File

@@ -82,7 +82,12 @@
width: 5%; width: 5%;
} }
.AkebiIcon, #ExtrasMenuButton {
width: 5%;
padding: 0 20px;
}
.ExtrasIcon,
.ServerIcon { .ServerIcon {
height: 20px; height: 20px;
filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%); filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%);
@@ -109,17 +114,17 @@
} }
@media (max-width: 1040px) { @media (max-width: 1040px) {
#playButton { #playButton {
right: 5%; right: 5%;
} }
} }
@media (max-width: 870px) { @media (max-width: 870px) {
#playButton { #playButton {
min-width: 235px; min-width: 235px;
} }
#officialPlay { #officialPlay {
width: 40%; width: 40%;
} }
} }

View File

@@ -8,33 +8,39 @@ import { translate } from '../../utils/language'
import { invoke } from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
import Server from '../../resources/icons/server.svg' 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 './ServerLaunchSection.css'
import {dataDir} from '@tauri-apps/api/path' import { dataDir } from '@tauri-apps/api/path'
import { getGameExecutable } from '../../utils/game' import { getGameExecutable } from '../../utils/game'
import { patchGame, unpatchGame } from '../../utils/metadata' import { patchGame, unpatchGame } from '../../utils/metadata'
interface IState { interface IProps {
grasscutterEnabled: boolean; openExtras: (playGame: () => void) => void
buttonLabel: string;
checkboxLabel: string;
ip: string;
port: string;
ipPlaceholder: string;
portPlaceholder: string;
portHelpText: string;
httpsLabel: string;
httpsEnabled: boolean;
swag: boolean;
} }
export default class ServerLaunchSection extends React.Component<{}, IState> { interface IState {
constructor(props: {}) { 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) super(props)
this.state = { this.state = {
@@ -48,12 +54,13 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
portHelpText: '', portHelpText: '',
httpsLabel: '', httpsLabel: '',
httpsEnabled: false, httpsEnabled: false,
swag: false swag: false,
akebiSet: false,
migotoSet: false,
} }
this.toggleGrasscutter = this.toggleGrasscutter.bind(this) this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
this.playGame = this.playGame.bind(this) this.playGame = this.playGame.bind(this)
this.launchAkebi = this.launchAkebi.bind(this)
this.setIp = this.setIp.bind(this) this.setIp = this.setIp.bind(this)
this.setPort = this.setPort.bind(this) this.setPort = this.setPort.bind(this)
this.toggleHttps = this.toggleHttps.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'), portHelpText: await translate('help.port_help_text'),
httpsLabel: await translate('main.https_enable'), httpsLabel: await translate('main.https_enable'),
httpsEnabled: config.https_enabled || false, 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 // Set state as well
this.setState({ this.setState({
grasscutterEnabled: config.toggle_grasscutter grasscutterEnabled: config.toggle_grasscutter,
}) })
await saveConfig(config) await saveConfig(config)
@@ -93,18 +102,20 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
async playGame(exe?: string, proc_name?: string) { async playGame(exe?: string, proc_name?: string) {
const config = await getConfig() const config = await getConfig()
if(!await getGameExecutable()) { if (!(await getGameExecutable())) {
alert('Game executable not set!') alert('Game executable not set!')
return return
} }
// Connect to proxy // Connect to proxy
if (config.toggle_grasscutter) { if (config.toggle_grasscutter) {
const patched = await patchGame() if (config.patch_metadata) {
const patched = await patchGame()
if (!patched) { if (!patched) {
alert('Could not patch game!') alert('Could not patch game!')
return return
}
} }
const game_exe = await getGameExecutable() 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_ip', this.state.ip)
await setConfigOption('last_port', this.state.port) 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', { await invoke('enable_process_watcher', {
process: proc_name || game_exe process: proc_name || game_exe,
}) })
// Connect to proxy if (config.use_internal_proxy) {
await invoke('connect', { port: 8365, certificatePath: await dataDir() + '\\cultivation\\ca' }) // 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' })
}
// Open server as well if the options are set // Open server as well if the options are set
if (config.grasscutter_with_game) { if (config.grasscutter_with_game) {
@@ -132,24 +147,26 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
await invoke('run_jar', { await invoke('run_jar', {
path: config.grasscutter_path, path: config.grasscutter_path,
executeIn: jarFolder, executeIn: jarFolder,
javaPath: config.java_path || '' javaPath: config.java_path || '',
}) })
} }
} else { } else {
const unpatched = await unpatchGame() const unpatched = await unpatchGame()
if (!unpatched) { 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 return
} }
} }
// Launch the program // Launch the program
const gameExists = await invoke('dir_exists', { 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)) 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', { await invoke('run_jar', {
path: config.grasscutter_path, path: config.grasscutter_path,
executeIn: jarFolder, 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) { setIp(text: string) {
this.setState({ this.setState({
ip: text ip: text,
}) })
} }
setPort(text: string) { setPort(text: string) {
this.setState({ this.setState({
port: text port: text,
}) })
} }
@@ -203,7 +210,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
// Set state as well // Set state as well
this.setState({ this.setState({
httpsEnabled: config.https_enabled httpsEnabled: config.https_enabled,
}) })
await saveConfig(config) await saveConfig(config)
@@ -213,34 +220,54 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
return ( return (
<div id="playButton"> <div id="playButton">
<div id="serverControls"> <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> </div>
{ {this.state.grasscutterEnabled && (
this.state.grasscutterEnabled && ( <div>
<div> <div className="ServerConfig" id="serverConfigContainer">
<div className="ServerConfig" id="serverConfigContainer"> <TextInput
<TextInput id="ip" key="ip" placeholder={this.state.ipPlaceholder} onChange={this.setIp} initalValue={this.state.ip} /> id="ip"
<TextInput style={{ key="ip"
placeholder={this.state.ipPlaceholder}
onChange={this.setIp}
initalValue={this.state.ip}
/>
<TextInput
style={{
width: '10%', width: '10%',
}} id="port" key="port" placeholder={this.state.portPlaceholder} onChange={this.setPort} initalValue={this.state.port} /> }}
<HelpButton contents={this.state.portHelpText} /> id="port"
<Checkbox id="httpsEnable" label={this.state.httpsLabel} onChange={this.toggleHttps} checked={this.state.httpsEnabled} /> key="port"
</div> 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}
/>
</div> </div>
) </div>
} )}
<div className="ServerLaunchButtons" id="serverLaunchContainer"> <div className="ServerLaunchButtons" id="serverLaunchContainer">
<BigButton onClick={this.playGame} id="officialPlay">{this.state.buttonLabel}</BigButton> <BigButton onClick={this.playGame} id="officialPlay">
{ {this.state.buttonLabel}
this.state.swag && ( </BigButton>
<BigButton onClick={this.launchAkebi} id="akebiLaunch"> {this.state.swag && (
<img className="AkebiIcon" id="akebiIcon" src={Akebi} /> <BigButton onClick={() => this.props.openExtras(this.playGame)} id="ExtrasMenuButton">
</BigButton> <img className="ExtrasIcon" id="extrasIcon" src={Plus} />
) </BigButton>
} )}
<BigButton onClick={this.launchServer} id="serverLaunch"> <BigButton onClick={this.launchServer} id="serverLaunch">
<img className="ServerIcon" id="serverLaunchIcon" src={Server} /> <img className="ServerIcon" id="serverLaunchIcon" src={Server} />
</BigButton> </BigButton>
@@ -248,4 +275,4 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
</div> </div>
) )
} }
} }

View File

@@ -51,4 +51,4 @@
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }

View File

@@ -1,26 +1,21 @@
import React from 'react' import React from 'react'
import { app } from '@tauri-apps/api' import { app } from '@tauri-apps/api'
import { appWindow } from '@tauri-apps/api/window' import { appWindow } from '@tauri-apps/api/window'
import closeIcon from '../../resources/icons/close.svg' import { getConfig, setConfigOption } from '../../utils/configuration'
import minIcon from '../../resources/icons/min.svg'
import cogBtn from '../../resources/icons/cog.svg'
import downBtn from '../../resources/icons/download.svg'
import Tr from '../../utils/language' import Tr from '../../utils/language'
import './TopBar.css' 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 { interface IProps {
optFunc: () => void; children?: React.ReactNode | React.ReactNode[]
downFunc: () => void;
gameFunc: () => void;
} }
interface IState { interface IState {
version: string; version: string
clicks: number; clicks: number
intv: NodeJS.Timeout | null; intv: NodeJS.Timeout | null
} }
export default class TopBar extends React.Component<IProps, IState> { export default class TopBar extends React.Component<IProps, IState> {
@@ -30,7 +25,7 @@ export default class TopBar extends React.Component<IProps, IState> {
this.state = { this.state = {
version: '0.0.0', version: '0.0.0',
clicks: 0, clicks: 0,
intv: null intv: null,
} }
this.activateClick = this.activateClick.bind(this) this.activateClick = this.activateClick.bind(this)
@@ -59,10 +54,10 @@ export default class TopBar extends React.Component<IProps, IState> {
setTimeout(() => { setTimeout(() => {
// Gotta clear it so it goes back to regular colors // Gotta clear it so it goes back to regular colors
this.setState({ this.setState({
clicks: 0 clicks: 0,
}) })
}, 600) }, 600)
// Activate... SWAG MODE // Activate... SWAG MODE
await setConfigOption('swag_mode', true) await setConfigOption('swag_mode', true)
@@ -75,7 +70,7 @@ export default class TopBar extends React.Component<IProps, IState> {
if (this.state.clicks < 3) { if (this.state.clicks < 3) {
this.setState({ this.setState({
clicks: this.state.clicks + 1, clicks: this.state.clicks + 1,
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500) intv: setTimeout(() => this.setState({ clicks: 0 }), 1500),
}) })
return return
@@ -89,36 +84,30 @@ export default class TopBar extends React.Component<IProps, IState> {
<span data-tauri-drag-region> <span data-tauri-drag-region>
<Tr text="main.title" /> <Tr text="main.title" />
</span> </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>
{
/**
* 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 className="TopBtns" id="topBarButtonContainer"> <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" /> <img src={closeIcon} alt="close" />
</div> </div>
<div id="minBtn" onClick={this.handleMinimize} className='TopButton'> <div id="minBtn" onClick={this.handleMinimize} className="TopButton">
<img src={minIcon} alt="minimize" /> <img src={minIcon} alt="minimize" />
</div> </div>
<div id="settingsBtn" onClick={this.props.optFunc} className='TopButton'> {this.props.children}
<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> */}
</div> </div>
</div> </div>
) )
} }
} }

View File

@@ -27,4 +27,4 @@
.BigButton.disabled:hover { .BigButton.disabled:hover {
background: linear-gradient(#949494, #9c9c9c); background: linear-gradient(#949494, #9c9c9c);
} }

View File

@@ -2,14 +2,14 @@ import React from 'react'
import './BigButton.css' import './BigButton.css'
interface IProps { interface IProps {
children: React.ReactNode; children: React.ReactNode
onClick: () => unknown; onClick: () => unknown
id: string; id: string
disabled?: boolean; disabled?: boolean
} }
interface IState { interface IState {
disabled?: boolean; disabled?: boolean
} }
export default class BigButton extends React.Component<IProps, IState> { export default class BigButton extends React.Component<IProps, IState> {
@@ -17,7 +17,7 @@ export default class BigButton extends React.Component<IProps, IState> {
super(props) super(props)
this.state = { this.state = {
disabled: this.props.disabled disabled: this.props.disabled,
} }
this.handleClick = this.handleClick.bind(this) 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) { static getDerivedStateFromProps(props: IProps, _state: IState) {
return { return {
disabled: props.disabled disabled: props.disabled,
} }
} }
@@ -37,9 +37,13 @@ export default class BigButton extends React.Component<IProps, IState> {
render() { render() {
return ( 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 className="BigButtonText">{this.props.children}</div>
</div> </div>
) )
} }
} }

View File

@@ -1,4 +1,4 @@
.Checkbox input[type="checkbox"] { .Checkbox input[type='checkbox'] {
display: none; display: none;
} }
@@ -17,10 +17,10 @@
.CheckboxDisplay img { .CheckboxDisplay img {
height: 100%; 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 { .Checkbox label {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }

View File

@@ -4,10 +4,10 @@ import checkmark from '../../../resources/icons/check.svg'
import './Checkbox.css' import './Checkbox.css'
interface IProps { interface IProps {
label?: string, label?: string
checked: boolean, checked: boolean
onChange: () => void, onChange: () => void
id: string id?: string
} }
interface IState { interface IState {
@@ -19,14 +19,14 @@ export default class Checkbox extends React.Component<IProps, IState> {
super(props) super(props)
this.state = { this.state = {
checked: props.checked checked: props.checked,
} }
} }
static getDerivedStateFromProps(props: IProps, state: IState) { static getDerivedStateFromProps(props: IProps, state: IState) {
if (props.checked !== state.checked) { if (props.checked !== state.checked) {
return { return {
checked: props.checked checked: props.checked,
} }
} }
@@ -41,14 +41,12 @@ export default class Checkbox extends React.Component<IProps, IState> {
render() { render() {
return ( return (
<div className="Checkbox"> <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}> <label htmlFor={this.props.id}>
<div className="CheckboxDisplay"> <div className="CheckboxDisplay">{this.state.checked ? <img src={checkmark} alt="Checkmark" /> : null}</div>
{this.state.checked ? <img src={checkmark} alt='Checkmark' /> : null}
</div>
<span>{this.props.label || ''}</span> <span>{this.props.label || ''}</span>
</label> </label>
</div> </div>
) )
} }
} }

View File

@@ -24,4 +24,4 @@
.FileSelectIcon img { .FileSelectIcon img {
height: 100%; height: 100%;
} }

View File

@@ -14,7 +14,7 @@ interface IProps {
readonly?: boolean readonly?: boolean
placeholder?: string placeholder?: string
folder?: boolean folder?: boolean
customClearBehaviour?: () => void, customClearBehaviour?: () => void
openFolder?: string openFolder?: string
} }
@@ -31,7 +31,7 @@ export default class DirInput extends React.Component<IProps, IState> {
this.state = { this.state = {
value: props.value || '', value: props.value || '',
placeholder: this.props.placeholder || 'Select file or folder...', placeholder: this.props.placeholder || 'Select file or folder...',
folder: this.props.folder || false folder: this.props.folder || false,
} }
this.handleIconClick = this.handleIconClick.bind(this) this.handleIconClick = this.handleIconClick.bind(this)
@@ -54,8 +54,8 @@ export default class DirInput extends React.Component<IProps, IState> {
async componentDidMount() { async componentDidMount() {
if (!this.props.placeholder) { if (!this.props.placeholder) {
const translation = await translate('components.select_file') const translation = await translate('components.select_file')
this.setState( { this.setState({
placeholder: translation placeholder: translation,
}) })
} }
} }
@@ -65,15 +65,12 @@ export default class DirInput extends React.Component<IProps, IState> {
if (this.state.folder) { if (this.state.folder) {
path = await open({ path = await open({
directory: true directory: true,
}) })
} else { } else {
console.log(this.props.openFolder)
path = await open({ path = await open({
filters: [ filters: [{ name: 'Files', extensions: this.props.extensions || ['*'] }],
{ name: 'Files', extensions: this.props.extensions || ['*'] } defaultPath: this.props.openFolder,
],
defaultPath: this.props.openFolder
}) })
} }
@@ -81,7 +78,7 @@ export default class DirInput extends React.Component<IProps, IState> {
if (!path) return if (!path) return
this.setState({ this.setState({
value: path value: path,
}) })
if (this.props.onChange) this.props.onChange(path) if (this.props.onChange) this.props.onChange(path)
@@ -89,12 +86,13 @@ export default class DirInput extends React.Component<IProps, IState> {
render() { render() {
return ( return (
<div className='DirInput'> <div className="DirInput">
<TextInput <TextInput
value={this.state.value} value={this.state.value}
placeholder={this.state.placeholder} placeholder={this.state.placeholder}
clearable={this.props.clearable !== undefined ? this.props.clearable : true} 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 }) this.setState({ value: text })
if (this.props.onChange) this.props.onChange(text) if (this.props.onChange) this.props.onChange(text)
@@ -108,4 +106,4 @@ export default class DirInput extends React.Component<IProps, IState> {
</div> </div>
) )
} }
} }

View File

@@ -4,4 +4,4 @@
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 10px; padding: 10px;
} }

View File

@@ -5,7 +5,7 @@ import DownloadSection from './DownloadSection'
import './DownloadList.css' import './DownloadList.css'
interface IProps { interface IProps {
downloadManager: DownloadHandler; downloadManager: DownloadHandler
} }
export default class DownloadList extends React.Component<IProps, never> { export default class DownloadList extends React.Component<IProps, never> {
@@ -16,17 +16,14 @@ export default class DownloadList extends React.Component<IProps, never> {
render() { render() {
const list = this.props.downloadManager.getDownloads().map((download) => { const list = this.props.downloadManager.getDownloads().map((download) => {
return ( 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>
)
} }
} }

View File

@@ -26,4 +26,4 @@
.DownloadStatus { .DownloadStatus {
text-align: right; text-align: right;
} }

View File

@@ -5,8 +5,8 @@ import ProgressBar from './ProgressBar'
import './DownloadSection.css' import './DownloadSection.css'
interface IProps { interface IProps {
downloadManager: DownloadHandler; downloadManager: DownloadHandler
downloadName: string; downloadName: string
} }
export default class DownloadSection extends React.Component<IProps, never> { export default class DownloadSection extends React.Component<IProps, never> {
@@ -32,4 +32,4 @@ export default class DownloadSection extends React.Component<IProps, never> {
</div> </div>
) )
} }
} }

View File

@@ -30,4 +30,4 @@
right: -450%; right: -450%;
width: 200px; width: 200px;
height: 120px; height: 120px;
} }

View File

@@ -2,53 +2,32 @@ import React from 'react'
import './HelpButton.css' import './HelpButton.css'
import Help from '../../../resources/icons/help.svg' import Help from '../../../resources/icons/help.svg'
import MiniDialog from '../MiniDialog' import { translate } from '../../../utils/language'
interface IProps { interface IProps {
children?: React.ReactNode[] | React.ReactNode; children?: React.ReactNode[] | React.ReactNode
contents?: string contents?: string
id?: string id?: string
} }
interface IState { export default class HelpButton extends React.Component<IProps, never> {
opened: boolean
}
export default class HelpButton extends React.Component<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props) super(props)
this.state = { this.showAlert = this.showAlert.bind(this)
opened: false
}
this.setOpen = this.setOpen.bind(this)
this.setClosed = this.setClosed.bind(this)
} }
setOpen() { async showAlert() {
this.setState({ opened: true }) if (this.props.contents) alert(await translate(this.props.contents))
}
setClosed() {
this.setState({ opened: false })
} }
render() { render() {
return ( return (
<div className="HelpSection"> <div className="HelpSection">
<div className="HelpButton" onMouseEnter={this.setOpen} onMouseLeave={this.setClosed}> <div className="HelpButton" onClick={this.showAlert}>
<img src={Help} /> <img src={Help} />
</div> </div>
<div className="HelpContents" style={{
display: this.state.opened ? 'block' : 'none'
}}>
<MiniDialog closeFn={this.setClosed}>
{this.props.contents || this.props.children}
</MiniDialog>
</div>
</div> </div>
) )
} }
} }

View File

@@ -4,15 +4,16 @@ import Tr from '../../../utils/language'
import './ProgressBar.css' import './ProgressBar.css'
interface IProps { interface IProps {
downloadManager: DownloadHandler, downloadManager: DownloadHandler
withStats?: boolean
} }
interface IState { interface IState {
average: number, average: number
files: number, files: number
extracting: number, extracting: number
total: number, total: number
speed: string, speed: string
} }
/** /**
@@ -29,7 +30,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
files, files,
extracting, extracting,
total: totalSize, total: totalSize,
speed: '0 B/s' speed: '0 B/s',
} }
} }
@@ -51,28 +52,33 @@ export default class ProgressBar extends React.Component<IProps, IState> {
return ( return (
<div className="MainProgressBarWrapper"> <div className="MainProgressBarWrapper">
<div className="ProgressBar"> <div className="ProgressBar">
<div className="InnerProgress" style={{ <div
width: `${(() => { className="InnerProgress"
// Handles no files downloading style={{
if (this.state.files === 0) { width: `${(() => {
return '100' // Handles no files downloading
} if (this.state.files === 0 || this.state.average >= 100) {
return '100'
}
if (this.state.total <= 0) { if (this.state.total <= 0) {
return '0' return '0'
} }
return this.state.average return this.state.average
})()}%`, })()}%`,
}}></div> }}
></div>
</div> </div>
<div className="MainProgressText"> {(this.props.withStats === undefined || this.props.withStats) && (
<Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed}) <div className="MainProgressText">
<br /> <Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed})
<Tr text="main.files_extracting" /> {this.state.extracting} <br />
</div> <Tr text="main.files_extracting" /> {this.state.extracting}
</div>
)}
</div> </div>
) )
} }
} }

View File

@@ -1,4 +1,5 @@
.ProgressBar, .InnerProgress { .ProgressBar,
.InnerProgress {
border-radius: 4px; border-radius: 4px;
} }
@@ -91,4 +92,4 @@
.downloadStop:hover { .downloadStop:hover {
cursor: pointer; cursor: pointer;
} }

View File

@@ -1,20 +1,20 @@
import React from 'react' import React from 'react'
import { capitalize } from '../../../utils/string' import { capitalize } from '../../../utils/string'
import Stop from '../../../resources/icons/close.svg' import Stop from '../../../resources/icons/close.svg'
import './ProgressBar.css' import './ProgressBar.css'
import DownloadHandler from '../../../utils/download' import DownloadHandler from '../../../utils/download'
import { translate } from '../../../utils/language' import { translate } from '../../../utils/language'
interface IProps { interface IProps {
path: string, path: string
downloadManager: DownloadHandler, downloadManager: DownloadHandler
} }
interface IState { interface IState {
progress: number, progress: number
status: string, status: string
total: number, total: number
} }
export default class ProgressBar extends React.Component<IProps, IState> { 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) const prog = this.props.downloadManager.getDownloadProgress(this.props.path)
this.setState({ this.setState({
progress: prog?.progress || 0, 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, total: prog?.total || 0,
}) })
@@ -54,24 +54,29 @@ export default class ProgressBar extends React.Component<IProps, IState> {
render() { render() {
return ( return (
<div className="ProgressBarWrapper"> <div className="ProgressBarWrapper">
<div style={{ <div
width: '80%' style={{
}}> width: '80%',
}}
>
<div className="ProgressBar"> <div className="ProgressBar">
<div className="InnerProgress" style={{ <div
width: `${(() => { className="InnerProgress"
// Handles files with content-lengths of 0 style={{
if (this.state.status === 'finished') { width: `${(() => {
return '100' // Handles files with content-lengths of 0
} if (this.state.status === 'finished') {
return '100'
}
if (this.state.total <= 0) { if (this.state.total <= 0) {
return '0' return '0'
} }
return this.state.progress / this.state.total * 100 return (this.state.progress / this.state.total) * 100
})()}%`, })()}%`,
}}></div> }}
></div>
</div> </div>
<div className="DownloadControls"> <div className="DownloadControls">
<div onClick={this.stopDownload} className="downloadStop"> <div onClick={this.stopDownload} className="downloadStop">
@@ -80,10 +85,8 @@ export default class ProgressBar extends React.Component<IProps, IState> {
</div> </div>
</div> </div>
<div className="ProgressText"> <div className="ProgressText">{capitalize(this.state.status) || 'Waiting'}</div>
{capitalize(this.state.status) || 'Waiting'}
</div>
</div> </div>
) )
} }
} }

View File

@@ -17,7 +17,7 @@
display: inline-block; display: inline-block;
position: absolute; position: absolute;
right: 16%; right: 16%;
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%);
} }
@@ -28,4 +28,4 @@
.TextInputClear { .TextInputClear {
height: 100%; height: 100%;
} }

View File

@@ -4,15 +4,15 @@ import './TextInput.css'
import Close from '../../../resources/icons/close.svg' import Close from '../../../resources/icons/close.svg'
interface IProps { interface IProps {
value?: string; value?: string
initalValue?: string; initalValue?: string
placeholder?: string; placeholder?: string
onChange?: (value: string) => void; onChange?: (value: string) => void
readOnly?: boolean; readOnly?: boolean
id?: string; id?: string
clearable?: boolean; clearable?: boolean
customClearBehaviour?: () => void; customClearBehaviour?: () => void
style?: React.CSSProperties; style?: React.CSSProperties
} }
interface IState { interface IState {
@@ -24,14 +24,14 @@ export default class TextInput extends React.Component<IProps, IState> {
super(props) super(props)
this.state = { this.state = {
value: props.value || '' value: props.value || '',
} }
} }
async componentDidMount() { async componentDidMount() {
if (this.props.initalValue) { if (this.props.initalValue) {
this.setState({ this.setState({
value: this.props.initalValue value: this.props.initalValue,
}) })
} }
} }
@@ -43,26 +43,35 @@ export default class TextInput extends React.Component<IProps, IState> {
render() { render() {
return ( return (
<div className="TextInputWrapper" style={this.props.style || {}}> <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
this.setState({ value: e.target.value }) id={this.props?.id}
if (this.props.onChange) this.props.onChange(e.target.value) readOnly={this.props.readOnly || false}
}} /> placeholder={this.props.placeholder || ''}
{ className="TextInput"
this.props.clearable ? value={this.state.value}
<div className="TextClear" onClick={() => { 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={() => {
// Run custom behaviour first // Run custom behaviour first
if (this.props.customClearBehaviour) return this.props.customClearBehaviour() if (this.props.customClearBehaviour) return this.props.customClearBehaviour()
this.setState({ value: '' }) this.setState({ value: '' })
if (this.props.onChange) this.props.onChange('') if (this.props.onChange) this.props.onChange('')
this.forceUpdate() this.forceUpdate()
}}> }}
<img src={Close} className="TextInputClear" /> >
</div> : null <img src={Close} className="TextInputClear" />
} </div>
) : null}
</div> </div>
) )
} }
} }

View File

@@ -1,4 +1,3 @@
.Divider { .Divider {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -13,4 +12,4 @@
.DividerLine { .DividerLine {
width: 60%; width: 60%;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }

View File

@@ -10,4 +10,4 @@ export default class Divider extends React.Component {
</div> </div>
) )
} }
} }

View File

@@ -28,4 +28,4 @@
.DownloadMenuSection .HelpButton img { .DownloadMenuSection .HelpButton img {
filter: none; filter: none;
} }

View File

@@ -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 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 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 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 { interface IProps {
closeFn: () => void; closeFn: () => void
downloadManager: DownloadHandler; downloadManager: DownloadHandler
} }
interface IState { interface IState {
@@ -41,7 +41,7 @@ export default class Downloads extends React.Component<IProps, IState> {
resources_downloading: this.props.downloadManager.downloadingResources(), resources_downloading: this.props.downloadManager.downloadingResources(),
repo_downloading: this.props.downloadManager.downloadingRepo(), repo_downloading: this.props.downloadManager.downloadingRepo(),
grasscutter_set: false, grasscutter_set: false,
resources_exist: false resources_exist: false,
} }
this.getGrasscutterFolder = this.getGrasscutterFolder.bind(this) this.getGrasscutterFolder = this.getGrasscutterFolder.bind(this)
@@ -63,7 +63,7 @@ export default class Downloads extends React.Component<IProps, IState> {
if (!gc_path || gc_path === '') { if (!gc_path || gc_path === '') {
this.setState({ this.setState({
grasscutter_set: false, grasscutter_set: false,
resources_exist: false resources_exist: false,
}) })
return return
@@ -72,15 +72,17 @@ export default class Downloads extends React.Component<IProps, IState> {
const path = gc_path.substring(0, gc_path.lastIndexOf('\\')) const path = gc_path.substring(0, gc_path.lastIndexOf('\\'))
if (gc_path) { if (gc_path) {
const resources_exist: boolean = await invoke('dir_exists', { const resources_exist: boolean =
path: path + '\\resources' ((await invoke('dir_exists', {
}) as boolean && !(await invoke('dir_is_empty', { path: path + '\\resources',
path: path + '\\resources' })) as boolean) &&
})) as boolean (!(await invoke('dir_is_empty', {
path: path + '\\resources',
})) as boolean)
this.setState({ this.setState({
grasscutter_set: gc_path !== '', grasscutter_set: gc_path !== '',
resources_exist resources_exist,
}) })
} }
} }
@@ -109,8 +111,9 @@ export default class Downloads extends React.Component<IProps, IState> {
async downloadGrasscutterStableRepo() { async downloadGrasscutterStableRepo() {
const folder = await this.getGrasscutterFolder() const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{ this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons) await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
this.toggleButtons()
}) })
this.toggleButtons() this.toggleButtons()
@@ -118,8 +121,9 @@ export default class Downloads extends React.Component<IProps, IState> {
async downloadGrasscutterDevRepo() { async downloadGrasscutterDevRepo() {
const folder = await this.getGrasscutterFolder() const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{ this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons) await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
this.toggleButtons()
}) })
this.toggleButtons() this.toggleButtons()
@@ -127,20 +131,22 @@ export default class Downloads extends React.Component<IProps, IState> {
async downloadGrasscutterStable() { async downloadGrasscutterStable() {
const folder = await this.getGrasscutterFolder() const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', () =>{ this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons) await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
this.toggleButtons
}) })
// Also add repo download // Also add repo download
this.downloadGrasscutterStableRepo() this.downloadGrasscutterStableRepo()
this.toggleButtons() this.toggleButtons()
} }
async downloadGrasscutterLatest() { async downloadGrasscutterLatest() {
const folder = await this.getGrasscutterFolder() const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', () =>{ this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons) await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
this.toggleButtons()
}) })
// Also add repo download // Also add repo download
@@ -152,24 +158,25 @@ export default class Downloads extends React.Component<IProps, IState> {
async downloadResources() { async downloadResources() {
const folder = await this.getGrasscutterFolder() const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(RESOURCES_DOWNLOAD, folder + '\\resources.zip', async () => { this.props.downloadManager.addDownload(RESOURCES_DOWNLOAD, folder + '\\resources.zip', async () => {
// Delete the existing folder if it exists // Delete the existing folder if it exists
if (await invoke('dir_exists', { if (
path: folder + '\\resources' await invoke('dir_exists', {
})) { path: folder + '\\resources',
})
) {
await invoke('dir_delete', { 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 // Rename folder to resources
invoke('rename', { invoke('rename', {
path: folder + '\\Resources', path: folder + '\\Resources',
newName: 'resources' newName: 'resources',
})
this.toggleButtons()
}) })
this.toggleButtons()
}) })
this.toggleButtons() this.toggleButtons()
@@ -190,32 +197,36 @@ export default class Downloads extends React.Component<IProps, IState> {
render() { render() {
return ( return (
<Menu closeFn={this.props.closeFn} className="Downloads" heading="Downloads"> <Menu closeFn={this.props.closeFn} className="Downloads" heading="Downloads">
<div className='DownloadMenuSection' id="downloadMenuContainerGCStable"> <div className="DownloadMenuSection" id="downloadMenuContainerGCStable">
<div className='DownloadLabel' id="downloadMenuLabelGCStable"> <div className="DownloadLabel" id="downloadMenuLabelGCStable">
<Tr text={ <Tr
this.state.grasscutter_set ? 'downloads.grasscutter_stable' : 'downloads.grasscutter_stable_update' text={this.state.grasscutter_set ? 'downloads.grasscutter_stable' : 'downloads.grasscutter_stable_update'}
} /> />
<HelpButton> <HelpButton contents="help.gc_stable_jar" />
<Tr text="help.gc_stable_jar" />
</HelpButton>
</div> </div>
<div className='DownloadValue' id="downloadMenuButtonGCStable"> <div className="DownloadValue" id="downloadMenuButtonGCStable">
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterStable} id="grasscutterStableBtn" > <BigButton
disabled={this.state.grasscutter_downloading}
onClick={this.downloadGrasscutterStable}
id="grasscutterStableBtn"
>
<Tr text="components.download" /> <Tr text="components.download" />
</BigButton> </BigButton>
</div> </div>
</div> </div>
<div className='DownloadMenuSection' id="downloadMenuContainerGCDev"> <div className="DownloadMenuSection" id="downloadMenuContainerGCDev">
<div className='DownloadLabel' id="downloadMenuLabelGCDev"> <div className="DownloadLabel" id="downloadMenuLabelGCDev">
<Tr text={ <Tr
this.state.grasscutter_set ? 'downloads.grasscutter_latest' : 'downloads.grasscutter_latest_update' text={this.state.grasscutter_set ? 'downloads.grasscutter_latest' : 'downloads.grasscutter_latest_update'}
} /> />
<HelpButton> <HelpButton contents="help.gc_dev_jar" />
<Tr text="help.gc_dev_jar" />
</HelpButton>
</div> </div>
<div className='DownloadValue' id="downloadMenuButtonGCDev"> <div className="DownloadValue" id="downloadMenuButtonGCDev">
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterLatest} id="grasscutterLatestBtn" > <BigButton
disabled={this.state.grasscutter_downloading}
onClick={this.downloadGrasscutterLatest}
id="grasscutterLatestBtn"
>
<Tr text="components.download" /> <Tr text="components.download" />
</BigButton> </BigButton>
</div> </div>
@@ -223,32 +234,44 @@ export default class Downloads extends React.Component<IProps, IState> {
<Divider /> <Divider />
<div className='DownloadMenuSection' id="downloadMenuContainerGCStableData"> <div className="DownloadMenuSection" id="downloadMenuContainerGCStableData">
<div className='DownloadLabel' id="downloadMenuLabelGCStableData"> <div className="DownloadLabel" id="downloadMenuLabelGCStableData">
<Tr text={ <Tr
this.state.grasscutter_set ? 'downloads.grasscutter_stable_data' : 'downloads.grasscutter_stable_data_update' text={
} /> this.state.grasscutter_set
<HelpButton> ? 'downloads.grasscutter_stable_data'
<Tr text="help.gc_stable_data" /> : 'downloads.grasscutter_stable_data_update'
</HelpButton> }
/>
<HelpButton contents="help.gc_stable_data" />
</div> </div>
<div className='DownloadValue' id="downloadMenuButtonGCStableData"> <div className="DownloadValue" id="downloadMenuButtonGCStableData">
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterStableRepo" > <BigButton
disabled={this.state.repo_downloading}
onClick={this.downloadGrasscutterStableRepo}
id="grasscutterStableRepo"
>
<Tr text="components.download" /> <Tr text="components.download" />
</BigButton> </BigButton>
</div> </div>
</div> </div>
<div className='DownloadMenuSection' id="downloadMenuContainerGCDevData"> <div className="DownloadMenuSection" id="downloadMenuContainerGCDevData">
<div className='DownloadLabel' id="downloadMenuLabelGCDevData"> <div className="DownloadLabel" id="downloadMenuLabelGCDevData">
<Tr text={ <Tr
this.state.grasscutter_set ? 'downloads.grasscutter_latest_data' : 'downloads.grasscutter_latest_data_update' text={
} /> this.state.grasscutter_set
<HelpButton> ? 'downloads.grasscutter_latest_data'
<Tr text="help.gc_dev_data" /> : 'downloads.grasscutter_latest_data_update'
</HelpButton> }
/>
<HelpButton contents="help.gc_dev_data" />
</div> </div>
<div className='DownloadValue' id="downloadMenuButtonGCDevData"> <div className="DownloadValue" id="downloadMenuButtonGCDevData">
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterDevRepo" > <BigButton
disabled={this.state.repo_downloading}
onClick={this.downloadGrasscutterStableRepo}
id="grasscutterDevRepo"
>
<Tr text="components.download" /> <Tr text="components.download" />
</BigButton> </BigButton>
</div> </div>
@@ -256,15 +279,17 @@ export default class Downloads extends React.Component<IProps, IState> {
<Divider /> <Divider />
<div className='DownloadMenuSection' id="downloadMenuContainerResources"> <div className="DownloadMenuSection" id="downloadMenuContainerResources">
<div className='DownloadLabel' id="downloadMenuLabelResources"> <div className="DownloadLabel" id="downloadMenuLabelResources">
<Tr text="downloads.resources" /> <Tr text="downloads.resources" />
<HelpButton> <HelpButton contents="help.resources" />
<Tr text="help.resources" />
</HelpButton>
</div> </div>
<div className='DownloadValue' id="downloadMenuButtonResources"> <div className="DownloadValue" id="downloadMenuButtonResources">
<BigButton disabled={this.state.resources_downloading || !this.state.grasscutter_set || this.state.resources_exist} onClick={this.downloadResources} id="resourcesBtn" > <BigButton
disabled={this.state.resources_downloading || !this.state.grasscutter_set || this.state.resources_exist}
onClick={this.downloadResources}
id="resourcesBtn"
>
<Tr text="components.download" /> <Tr text="components.download" />
</BigButton> </BigButton>
</div> </div>
@@ -272,4 +297,4 @@ export default class Downloads extends React.Component<IProps, IState> {
</Menu> </Menu>
) )
} }
} }

View 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;
}

View 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>
)
}
}

View File

@@ -34,4 +34,4 @@
.GameDownloadDir .DirInput .TextInputWrapper input { .GameDownloadDir .DirInput .TextInputWrapper input {
width: 80%; width: 80%;
} }

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import Menu from './Menu' import Menu from './Menu'
import Tr, { translate } from '../../../utils/language' import { translate } from '../../../utils/language'
import DownloadHandler from '../../../utils/download' import DownloadHandler from '../../../utils/download'
import './Game.css' import './Game.css'
@@ -12,14 +12,14 @@ import { unzip } from '../../../utils/zipUtils'
const GAME_DOWNLOAD = '' const GAME_DOWNLOAD = ''
interface IProps { interface IProps {
closeFn: () => void; closeFn: () => void
downloadManager: DownloadHandler; downloadManager: DownloadHandler
} }
interface IState { interface IState {
gameDownloading: boolean; gameDownloading: boolean
gameDownloadFolder: string; gameDownloadFolder: string
dirPlaceholder: string; dirPlaceholder: string
} }
export default class Downloads extends React.Component<IProps, IState> { export default class Downloads extends React.Component<IProps, IState> {
@@ -29,7 +29,7 @@ export default class Downloads extends React.Component<IProps, IState> {
this.state = { this.state = {
gameDownloading: false, gameDownloading: false,
gameDownloadFolder: '', gameDownloadFolder: '',
dirPlaceholder: '' dirPlaceholder: '',
} }
this.downloadGame = this.downloadGame.bind(this) this.downloadGame = this.downloadGame.bind(this)
@@ -37,7 +37,7 @@ export default class Downloads extends React.Component<IProps, IState> {
async componentDidMount() { async componentDidMount() {
this.setState({ this.setState({
dirPlaceholder: await translate('components.select_folder') dirPlaceholder: await translate('components.select_folder'),
}) })
console.log(this.state) console.log(this.state)
@@ -45,39 +45,48 @@ export default class Downloads extends React.Component<IProps, IState> {
async downloadGame() { async downloadGame() {
const folder = this.state.gameDownloadFolder const folder = this.state.gameDownloadFolder
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', () =>{ this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', async () => {
unzip(folder + '\\game.zip', folder + '\\', () => { await unzip(folder + '\\game.zip', folder + '\\', true)
this.setState({ this.setState({
gameDownloading: false gameDownloading: false,
})
}) })
}) })
this.setState({ this.setState({
gameDownloading: true gameDownloading: true,
}) })
} }
render() { render() {
return ( return (
<Menu heading='Download Game' closeFn={this.props.closeFn} className="GameDownloadMenu"> <Menu heading="Download Game" closeFn={this.props.closeFn} className="GameDownloadMenu">
<div className="GameDownload"> <div className="GameDownload">
{ {this.state.gameDownloadFolder !== '' && !this.state.gameDownloading ? (
this.state.gameDownloadFolder !== '' && !this.state.gameDownloading ? <BigButton id="downloadGameBtn" onClick={this.downloadGame}>
<BigButton id="downloadGameBtn" onClick={this.downloadGame}>Download Game</BigButton> Download Game
: <BigButton id="disabledGameBtn" onClick={() => null} disabled>Download Game</BigButton> </BigButton>
} ) : (
<HelpButton> <BigButton id="disabledGameBtn" onClick={() => null} disabled>
<Tr text="main.game_help_text" /> Download Game
</HelpButton> </BigButton>
)}
<HelpButton contents="main.game_help_text" />
</div> </div>
<div className="GameDownloadDir"> <div className="GameDownloadDir">
<DirInput folder placeholder={this.state.dirPlaceholder} clearable={false} readonly={true} onChange={(value: string) => this.setState({ <DirInput
gameDownloadFolder: value folder
})}/> placeholder={this.state.dirPlaceholder}
clearable={false}
readonly={true}
onChange={(value: string) =>
this.setState({
gameDownloadFolder: value,
})
}
/>
</div> </div>
</Menu> </Menu>
) )
} }
} }

View File

@@ -12,7 +12,7 @@
background: #fff; background: #fff;
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.2); box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.2);
overflow-y: auto; overflow-y: auto;
@@ -52,4 +52,4 @@
.MenuExit img { .MenuExit img {
height: 100%; height: 100%;
} }

View File

@@ -4,10 +4,10 @@ import './Menu.css'
import Close from '../../../resources/icons/close.svg' import Close from '../../../resources/icons/close.svg'
interface IProps { interface IProps {
children: React.ReactNode[] | React.ReactNode; children: React.ReactNode[] | React.ReactNode
className?: string; className?: string
heading: string; heading: string
closeFn: () => void; closeFn: () => void
} }
export default class Menu extends React.Component<IProps, never> { export default class Menu extends React.Component<IProps, never> {
@@ -18,16 +18,18 @@ export default class Menu extends React.Component<IProps, never> {
render() { render() {
return ( return (
<div className={'Menu ' + this.props.className} id="menuContainer"> <div className={'Menu ' + this.props.className} id="menuContainer">
<div className='MenuTop' id="menuContainerTop"> <div className="MenuTop" id="menuContainerTop">
<div className="MenuHeading" id="menuHeading">{this.props.heading}</div> <div className="MenuHeading" id="menuHeading">
{this.props.heading}
</div>
<div className="MenuExit" id="menuButtonCloseContainer" onClick={this.props.closeFn}> <div className="MenuExit" id="menuButtonCloseContainer" onClick={this.props.closeFn}>
<img src={Close} className="MenuClose" id="menuButtonCloseIcon" /> <img src={Close} className="MenuClose" id="menuButtonCloseIcon" />
</div> </div>
</div> </div>
<div className='MenuInner' id="menuContent"> <div className="MenuInner" id="menuContent">
{this.props.children} {this.props.children}
</div> </div>
</div> </div>
) )
} }
} }

View File

@@ -15,4 +15,8 @@
.OptionSection .BigButtonText { .OptionSection .BigButtonText {
font-size: 12px; font-size: 12px;
} }
.OptionSection .HelpButton img {
filter: invert(0%) sepia(91%) saturate(7464%) hue-rotate(101deg) brightness(0%) contrast(107%);
}

View File

@@ -14,10 +14,11 @@ import './Options.css'
import BigButton from '../common/BigButton' import BigButton from '../common/BigButton'
import DownloadHandler from '../../../utils/download' import DownloadHandler from '../../../utils/download'
import * as meta from '../../../utils/metadata' import * as meta from '../../../utils/metadata'
import HelpButton from '../common/HelpButton'
interface IProps { interface IProps {
closeFn: () => void; closeFn: () => void
downloadManager: DownloadHandler; downloadManager: DownloadHandler
} }
interface IState { interface IState {
@@ -25,16 +26,20 @@ interface IState {
grasscutter_path: string grasscutter_path: string
java_path: string java_path: string
grasscutter_with_game: boolean grasscutter_with_game: boolean
language_options: { [key: string]: string }[], language_options: { [key: string]: string }[]
current_language: string current_language: string
bg_url_or_path: string bg_url_or_path: string
themes: string[] themes: string[]
theme: string theme: string
encryption: boolean encryption: boolean
patch_metadata: boolean
use_internal_proxy: boolean
swag: boolean swag: boolean
// Swag stuff // Swag stuff
akebi_path: string akebi_path: string
migoto_path: string
reshade_path: string
} }
export default class Options extends React.Component<IProps, IState> { export default class Options extends React.Component<IProps, IState> {
@@ -52,20 +57,27 @@ export default class Options extends React.Component<IProps, IState> {
themes: ['default'], themes: ['default'],
theme: '', theme: '',
encryption: false, encryption: false,
patch_metadata: false,
use_internal_proxy: false,
swag: false, swag: false,
// Swag stuff // Swag stuff
akebi_path: '', akebi_path: '',
migoto_path: '',
reshade_path: '',
} }
this.setGameExecutable = this.setGameExecutable.bind(this) this.setGameExecutable = this.setGameExecutable.bind(this)
this.setGrasscutterJar = this.setGrasscutterJar.bind(this) this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
this.setJavaPath = this.setJavaPath.bind(this) this.setJavaPath = this.setJavaPath.bind(this)
this.setAkebi = this.setAkebi.bind(this) this.setAkebi = this.setAkebi.bind(this)
this.setMigoto = this.setMigoto.bind(this)
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this) this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
this.setCustomBackground = this.setCustomBackground.bind(this) this.setCustomBackground = this.setCustomBackground.bind(this)
this.toggleEncryption = this.toggleEncryption.bind(this) this.toggleEncryption = this.toggleEncryption.bind(this)
this.restoreMetadata = this.restoreMetadata.bind(this) this.restoreMetadata = this.restoreMetadata.bind(this)
this.toggleMetadata = this.toggleMetadata.bind(this)
this.toggleProxy = this.toggleProxy.bind(this)
} }
async componentDidMount() { async componentDidMount() {
@@ -88,10 +100,14 @@ export default class Options extends React.Component<IProps, IState> {
themes: (await getThemeList()).map((t) => t.name), themes: (await getThemeList()).map((t) => t.name),
theme: config.theme || 'default', theme: config.theme || 'default',
encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled'), 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: config.swag_mode || false,
// Swag stuff // Swag stuff
akebi_path: config.akebi_path || '', akebi_path: config.akebi_path || '',
migoto_path: config.migoto_path || '',
reshade_path: config.reshade_path || '',
}) })
this.forceUpdate() this.forceUpdate()
@@ -125,7 +141,23 @@ export default class Options extends React.Component<IProps, IState> {
setConfigOption('akebi_path', value) setConfigOption('akebi_path', value)
this.setState({ 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() { async restoreMetadata() {
console.log(this.props)
await meta.restoreMetadata(this.props.downloadManager) await meta.restoreMetadata(this.props.downloadManager)
} }
async installCert() { async installCert() {
await invoke('generate_ca_files', { 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="OptionSection" id="menuOptionsContainermetaDownload">
<div className="OptionLabel" id="menuOptionsLabelmetaDownload"> <div className="OptionLabel" id="menuOptionsLabelmetaDownload">
<Tr text="options.recover_metadata" /> <Tr text="options.recover_metadata" />
<HelpButton contents="help.emergency_metadata" />
</div> </div>
<div className="OptionValue" id="menuOptionsButtonmetaDownload"> <div className="OptionValue" id="menuOptionsButtonmetaDownload">
<BigButton onClick={this.restoreMetadata} id="metaDownload"> <BigButton onClick={this.restoreMetadata} id="metaDownload">
<Tr text='components.download' /> <Tr text="components.download" />
</BigButton> </BigButton>
</div> </div>
</div> </div>
<div className='OptionSection' id="menuOptionsContainerGCJar"> <div className="OptionSection" id="menuOptionsContainerPatchMeta">
<div className='OptionLabel' id="menuOptionsLabelGCJar"> <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" /> <Tr text="options.grasscutter_jar" />
</div> </div>
<div className="OptionValue" id="menuOptionsDirGCJar"> <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="OptionSection" id="menuOptionsContainerToggleEnc">
<div className="OptionLabel" id="menuOptionsLabelToggleEnc"> <div className="OptionLabel" id="menuOptionsLabelToggleEnc">
<Tr text="options.toggle_encryption" /> <Tr text="options.toggle_encryption" />
<HelpButton contents="help.encryption" />
</div> </div>
<div className="OptionValue" id="menuOptionsButtonToggleEnc"> <div className="OptionValue" id="menuOptionsButtonToggleEnc">
<BigButton onClick={this.toggleEncryption} id="toggleEnc"> <BigButton onClick={this.toggleEncryption} id="toggleEnc">
@@ -244,31 +318,45 @@ export default class Options extends React.Component<IProps, IState> {
</BigButton> </BigButton>
</div> </div>
</div> </div>
<div className='OptionSection' id="menuOptionsContainerInstallCert"> <div className="OptionSection" id="menuOptionsContainerInstallCert">
<div className='OptionLabel' id="menuOptionsLabelInstallCert"> <div className="OptionLabel" id="menuOptionsLabelInstallCert">
<Tr text="options.install_certificate" /> <Tr text="options.install_certificate" />
</div> </div>
<div className='OptionValue' id="menuOptionsButtonInstallCert"> <div className="OptionValue" id="menuOptionsButtonInstallCert">
<BigButton disabled={false} onClick={this.installCert} id="installCert"> <BigButton disabled={false} onClick={this.installCert} id="installCert">
<Tr text="components.install" /> <Tr text="components.install" />
</BigButton> </BigButton>
</div> </div>
</div> </div>
{ {this.state.swag && (
this.state.swag && ( <>
<> <Divider />
<Divider /> <div className="OptionSection" id="menuOptionsContainerAkebi">
<div className='OptionSection' id="menuOptionsContainerAkebi"> <div className="OptionLabel" id="menuOptionsLabelAkebi">
<div className='OptionLabel' id="menuOptionsLabelAkebi"> <Tr text="swag.akebi" />
<Tr text="swag.akebi" />
</div>
<div className='OptionValue' id="menuOptionsDirAkebi">
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
</div>
</div> </div>
</> <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 /> <Divider />

View 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);
}
}

View 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>
)
}
}

View 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;
}

View 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>
)
}
}

View 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;
}

View 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>
)
}
}

View 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);
}

View 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>
)
}
}

View File

@@ -1,97 +1,97 @@
.NewsSection { .NewsSection {
background-color: rgba(106, 105, 106, 0.6); background-color: rgba(106, 105, 106, 0.6);
position: absolute; position: absolute;
min-height: 219px; min-height: 219px;
height: 40%; height: 40%;
width: 512px; width: 512px;
bottom: 35%; bottom: 35%;
left: 5%; left: 5%;
} }
@media (max-width: 830px) { @media (max-width: 830px) {
.NewsSection { .NewsSection {
width: 61%; width: 61%;
} }
} }
.NewsTabs { .NewsTabs {
background-color: rgba(77, 77, 77, 0.6); background-color: rgba(77, 77, 77, 0.6);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;
width: 100%; width: 100%;
height: 43px; height: 43px;
} }
.NewsTab { .NewsTab {
height: 50%; height: 50%;
margin: 0 10px; margin: 0 10px;
text-align: center; text-align: center;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
} }
.NewsTab:hover { .NewsTab:hover {
cursor: pointer; cursor: pointer;
} }
.NewsTab.selected { .NewsTab.selected {
border-bottom: 2px solid #ffc61e; border-bottom: 2px solid #ffc61e;
} }
.NewsContent { .NewsContent {
display: block; display: block;
height: calc(100% - 43px); height: calc(100% - 43px);
width: 100%; width: 100%;
color: #fff; color: #fff;
} }
.NewsContent tbody { .NewsContent tbody {
display: inline-block; display: inline-block;
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
} }
.NewsContent tbody::-webkit-scrollbar { .NewsContent tbody::-webkit-scrollbar {
display: none; display: none;
} }
.Commit { .Commit {
margin: 0; margin: 0;
color: #fff; color: #fff;
max-height: 42px; max-height: 42px;
} }
.CommitAuthor { .CommitAuthor {
width: calc(100% * 0.4); width: calc(100% * 0.4);
padding-left: 5px; padding-left: 5px;
vertical-align: top; vertical-align: top;
font-weight: bold; font-weight: bold;
} }
.CommitMessage { .CommitMessage {
width: calc(100% * 0.6); width: calc(100% * 0.6);
} }
.CommitMessage span { .CommitMessage span {
display: -webkit-box; display: -webkit-box;
width: 100%; width: 100%;
max-height: 42px; max-height: 42px;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
margin-bottom: 5px; margin-bottom: 5px;
} }

View File

@@ -6,33 +6,33 @@ import Tr from '../../../utils/language'
import './NewsSection.css' import './NewsSection.css'
interface IProps { interface IProps {
selected?: string; selected?: string
} }
interface IState { interface IState {
selected: string; selected: string
news?: JSX.Element; news?: JSX.Element
commitList?: JSX.Element[]; commitList?: JSX.Element[]
} }
interface GrasscutterAPIResponse { interface GrasscutterAPIResponse {
commits: { commits: {
gc_stable: CommitResponse[]; gc_stable: CommitResponse[]
gc_dev: CommitResponse[]; gc_dev: CommitResponse[]
cultivation: CommitResponse[]; cultivation: CommitResponse[]
} }
} }
interface CommitResponse { interface CommitResponse {
sha: string; sha: string
commit: Commit; commit: Commit
} }
interface Commit { interface Commit {
author: { author: {
name: string; name: string
}; }
message: string; message: string
} }
export default class NewsSection extends React.Component<IProps, IState> { export default class NewsSection extends React.Component<IProps, IState> {
@@ -65,14 +65,16 @@ export default class NewsSection extends React.Component<IProps, IState> {
try { try {
grasscutterApiResponse = JSON.parse(response) grasscutterApiResponse = JSON.parse(response)
} catch(e) { } catch (e) {
grasscutterApiResponse = null grasscutterApiResponse = null
} }
let commits: CommitResponse[] let commits: CommitResponse[]
if (grasscutterApiResponse?.commits == null) { if (grasscutterApiResponse?.commits == null) {
// If it didn't work, use official API // 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) commits = JSON.parse(response)
} else { } else {
commits = grasscutterApiResponse.commits.gc_stable commits = grasscutterApiResponse.commits.gc_stable
@@ -80,21 +82,25 @@ export default class NewsSection extends React.Component<IProps, IState> {
// Probably rate-limited // Probably rate-limited
if (!Array.isArray(commits)) return if (!Array.isArray(commits)) return
// Get only first 5 // Get only first 5
const commitsList = commits.slice(0, 10) const commitsList = commits.slice(0, 10)
const commitsListHtml = commitsList.map((commitResponse: CommitResponse) => { const commitsListHtml = commitsList.map((commitResponse: CommitResponse) => {
return ( return (
<tr className="Commit" id="newsCommitsTable" key={commitResponse.sha}> <tr className="Commit" id="newsCommitsTable" key={commitResponse.sha}>
<td className="CommitAuthor"><span>{commitResponse.commit.author.name}</span></td> <td className="CommitAuthor">
<td className="CommitMessage"><span>{commitResponse.commit.message}</span></td> <span>{commitResponse.commit.author.name}</span>
</td>
<td className="CommitMessage">
<span>{commitResponse.commit.message}</span>
</td>
</tr> </tr>
) )
}) })
this.setState({ this.setState({
commitList: commitsListHtml, commitList: commitsListHtml,
news: <>{commitsListHtml}</> news: <>{commitsListHtml}</>,
}) })
} }
@@ -104,7 +110,7 @@ export default class NewsSection extends React.Component<IProps, IState> {
async showNews() { async showNews() {
let news: JSX.Element | JSX.Element[] = <tr></tr> let news: JSX.Element | JSX.Element[] = <tr></tr>
switch(this.state.selected) { switch (this.state.selected) {
case 'commits': { case 'commits': {
const commits = await this.showLatestCommits() const commits = await this.showLatestCommits()
if (commits != null) { if (commits != null) {
@@ -114,16 +120,24 @@ export default class NewsSection extends React.Component<IProps, IState> {
} }
case 'latest_version': case 'latest_version':
news = <tr><td>Latest version</td></tr> news = (
<tr>
<td>Latest version</td>
</tr>
)
break break
default: default:
news = <tr><td>Unknown</td></tr> news = (
<tr>
<td>Unknown</td>
</tr>
)
break break
} }
this.setState({ this.setState({
news: <>{news}</> news: <>{news}</>,
}) })
} }
@@ -131,19 +145,25 @@ export default class NewsSection extends React.Component<IProps, IState> {
return ( return (
<div className="NewsSection" id="newsContainer"> <div className="NewsSection" id="newsContainer">
<div className="NewsTabs" id="newsTabsContainer"> <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" /> <Tr text="news.latest_commits" />
</div> </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" /> <Tr text="news.latest_version" />
</div> </div>
</div> </div>
<table className="NewsContent" id="newsContent"> <table className="NewsContent" id="newsContent">
<tbody> <tbody>{this.state.news}</tbody>
{this.state.news}
</tbody>
</table> </table>
</div> </div>
) )
} }
} }

Some files were not shown because too many files have changed in this diff Show More