Run prettier formatter

This commit is contained in:
Brian Bowman
2022-07-19 04:37:38 -05:00
parent e9df0f17db
commit eb9aa34323
67 changed files with 1157 additions and 1071 deletions

View File

@@ -1,48 +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"],
"prettier" "rules": {
], "@typescript-eslint/ban-types": [
"parser": "@typescript-eslint/parser", "warn",
"parserOptions": { {
"ecmaFeatures": { "extendDefaults": true,
"jsx": true "types": {
}, "{}": false
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"@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"
} }
}
} }

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

@@ -3,20 +3,20 @@ name: Check frontend
on: on:
push: push:
paths: paths:
- ".github/workflows/frontend-checks.yml" - '.github/workflows/frontend-checks.yml'
- "src/**" - 'src/**'
- ".eslintrc.json" - '.eslintrc.json'
- "package.json" - 'package.json'
- "tsconfig.json" - 'tsconfig.json'
- "yarn.lock" - 'yarn.lock'
pull_request: pull_request:
paths: paths:
- ".github/workflows/frontend-checks.yml" - '.github/workflows/frontend-checks.yml'
- "src/**" - 'src/**'
- ".eslintrc.json" - '.eslintrc.json'
- "package.json" - 'package.json'
- "tsconfig.json" - 'tsconfig.json'
- "yarn.lock" - 'yarn.lock'
concurrency: concurrency:
group: ${{ github.ref }}-${{ github.workflow }} group: ${{ github.ref }}-${{ github.workflow }}
@@ -34,4 +34,3 @@ jobs:
run: yarn tsc --noEmit run: yarn tsc --noEmit
- name: Run ESLint - name: Run ESLint
run: yarn eslint src run: yarn eslint src

View File

@@ -1,25 +1,28 @@
# Client 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)
* [Setup](#setup)
* [Building](#building)
* [Code Formatting and Linting](#code-formatting-and-linting)
* [Generating Update Artifacts](#generating-update-artifacts)
* [Theming](#theming)
* [Screenshots](#screenshots)
* [Credits](#credits)
- [Download](#download)
- [Developer Quick-start](#developer-quickstart)
- [Setup](#setup)
- [Building](#building)
- [Code Formatting and Linting](#code-formatting-and-linting)
- [Generating Update Artifacts](#generating-update-artifacts)
- [Theming](#theming)
- [Screenshots](#screenshots)
- [Credits](#credits)
# Download # 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.
@@ -27,27 +30,33 @@ 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) (cry about it `npm` lovers) - Install [NodeJS >12](https://nodejs.org/en/)
* Install [Rust](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
For a release build, For a release build,
- `yarn build` - `yarn build`
For a debug build, For a debug build,
- `yarn build --debug` - `yarn build --debug`
### Code Formatting and Linting ### Code Formatting and Linting
- `yarn format` - `yarn format`
- `yarn lint` - `yarn lint`
### Generating Update Artifacts ### 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. - Add the `TAURI_PRIVATE_KEY` as an environment variable with a path to your private key.
* `yarn build` - 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` The update will be at `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x64_xx-XX.msi.zip`
@@ -56,6 +65,7 @@ The update will be at `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x6
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)
@@ -63,8 +73,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

@@ -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>

View File

@@ -1,61 +1,61 @@
{ {
"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_executable": "Spiel Datei auswählen",
"grasscutter_jar": "Grasscuter JAR auswählen", "grasscutter_jar": "Grasscuter JAR auswählen",
"toggle_encryption": "Verschlüsselung umschalten", "toggle_encryption": "Verschlüsselung umschalten",
"java_path": "Benutzerdefinierten Java Pfad setzen", "java_path": "Benutzerdefinierten Java Pfad setzen",
"grasscutter_with_game": "Grasscutter automatisch mit dem Spiel starten", "grasscutter_with_game": "Grasscutter automatisch mit dem Spiel starten",
"language": "Sprache auswählen", "language": "Sprache auswählen",
"background": "Benutzerdefinierten Hintergrund festlegen (link oder bild)", "background": "Benutzerdefinierten Hintergrund festlegen (link oder bild)",
"theme": "Theme auswählen" "theme": "Theme auswählen"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "Stabile Grasscutter Daten herunterladen", "grasscutter_stable_data": "Stabile Grasscutter Daten herunterladen",
"grasscutter_latest_data": "Aktuellste Grasscutter Daten herunterladen", "grasscutter_latest_data": "Aktuellste Grasscutter Daten herunterladen",
"grasscutter_stable_data_update": "Stabile Grasscutter Daten aktualisieren", "grasscutter_stable_data_update": "Stabile Grasscutter Daten aktualisieren",
"grasscutter_latest_data_update": "Aktuellste Grasscutter Daten aktualisieren", "grasscutter_latest_data_update": "Aktuellste Grasscutter Daten aktualisieren",
"grasscutter_stable": "Stabile Grasscutter Version herunterladen", "grasscutter_stable": "Stabile Grasscutter Version herunterladen",
"grasscutter_latest": "Aktuellste Grasscutter Version herunterladen", "grasscutter_latest": "Aktuellste Grasscutter Version herunterladen",
"grasscutter_stable_update": "Stabile Grasscutter Version aktualisieren", "grasscutter_stable_update": "Stabile Grasscutter Version aktualisieren",
"grasscutter_latest_update": "Aktuellste Grasscutter Version aktualisieren", "grasscutter_latest_update": "Aktuellste Grasscutter Version aktualisieren",
"resources": "Grasscutter Ressourcen herunterladen" "resources": "Grasscutter Ressourcen herunterladen"
}, },
"download_status": { "download_status": {
"downloading": "Lädt herunter", "downloading": "Lädt herunter",
"extracting": "Extrahiert", "extracting": "Extrahiert",
"error": "Fehler", "error": "Fehler",
"finished": "Fertig", "finished": "Fertig",
"stopped": "Gestoppt" "stopped": "Gestoppt"
}, },
"components": { "components": {
"select_file": "Datei oder Ordner auswählen...", "select_file": "Datei oder Ordner auswählen...",
"select_folder": "Ordner auswählen...", "select_folder": "Ordner auswählen...",
"download": "Herunterladen" "download": "Herunterladen"
}, },
"news": { "news": {
"latest_commits": "Letzte Commits", "latest_commits": "Letzte Commits",
"latest_version": "Letzte Version" "latest_version": "Letzte Version"
}, },
"help": { "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'.", "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'.",
"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.", "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_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_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_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.", "gc_dev_data": "Laden Sie die neuesten Grasscutter-Entwicklungsdateien herunter, welche keine Jar-Datei enthält. Dies ist nützlich zum Aktualisieren.",
"resources": "Diese werden auch benötigt, um einen Grasscutter-Server auszuführen. Diese Schaltfläche ist grau, wenn Sie einen bestehenden Ressourcenordner mit Inhalten haben" "resources": "Diese werden auch benötigt, um einen Grasscutter-Server auszuführen. Diese Schaltfläche ist grau, wenn Sie einen bestehenden Ressourcenordner mit Inhalten haben"
}
} }
}

View File

@@ -12,20 +12,12 @@
"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/*", "./mhycrypto.dll"],
"lang/*.json",
"keys/*",
"./mhycrypto.dll"
],
"targets": "all", "targets": "all",
"windows": { "windows": {
"allowDowngrades": false, "allowDowngrades": false,

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

@@ -22,7 +22,8 @@ select:focus {
border-bottom-color: #ffd326; border-bottom-color: #ffd326;
} }
#root, .App { #root,
.App {
height: 100%; height: 100%;
} }
@@ -82,28 +83,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

@@ -25,16 +25,16 @@ import { getTheme, loadTheme } from '../utils/themes'
import { unpatchGame } from '../utils/metadata' import { unpatchGame } from '../utils/metadata'
interface IProps { interface IProps {
[key: string]: never; [key: string]: never
} }
interface IState { interface IState {
isDownloading: boolean; isDownloading: boolean
optionsOpen: boolean; optionsOpen: boolean
miniDownloadsOpen: boolean; miniDownloadsOpen: boolean
downloadsOpen: boolean; downloadsOpen: boolean
gameDownloadsOpen: boolean; gameDownloadsOpen: boolean
bgFile: string; bgFile: string
} }
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile' const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
@@ -57,7 +57,7 @@ class App extends React.Component<IProps, IState> {
console.log(payload) console.log(payload)
}) })
listen('jar_extracted', ({ payload }: { payload: string}) => { listen('jar_extracted', ({ payload }: { payload: string }) => {
setConfigOption('grasscutter_path', payload) setConfigOption('grasscutter_path', payload)
}) })
@@ -71,7 +71,9 @@ class App extends React.Component<IProps, IState> {
console.log(`unpatched game? ${unpatched}`) console.log(`unpatched game? ${unpatched}`)
if (!unpatched) { if (!unpatched) {
alert(`Could not unpatch game! (You should be able to find your metadata backup in ${await dataDir()}\\cultivation\\)`) alert(
`Could not unpatch game! (You should be able to find your metadata backup in ${await dataDir()}\\cultivation\\)`
)
} }
} }
}) })
@@ -108,45 +110,55 @@ 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) { if (!cert_generated) {
// Generate the certificate // Generate the certificate
await invoke('generate_ca_files', { await invoke('generate_ca_files', {
path: await dataDir() + 'cultivation' path: (await dataDir()) + 'cultivation',
}) })
await setConfigOption('cert_generated', true) await setConfigOption('cert_generated', true)
@@ -155,18 +167,23 @@ class App extends React.Component<IProps, IState> {
// Period check to only show progress bar when downloading files // Period check to only show progress bar when downloading files
setInterval(() => { setInterval(() => {
this.setState({ this.setState({
isDownloading: downloadHandler.getDownloads().filter(d => d.status !== 'finished')?.length > 0 isDownloading: downloadHandler.getDownloads().filter((d) => d.status !== 'finished')?.length > 0,
}) })
}, 1000) }, 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
}> ? {
background: `url("${this.state.bgFile}") fixed`,
}
: {}
}
>
<TopBar <TopBar
optFunc={() => { optFunc={() => {
this.setState({ optionsOpen: !this.state.optionsOpen }) this.setState({ optionsOpen: !this.state.optionsOpen })
@@ -199,10 +216,7 @@ class App extends React.Component<IProps, IState> {
{ {
// Download menu // Download menu
this.state.downloadsOpen ? ( this.state.downloadsOpen ? (
<Downloads <Downloads downloadManager={downloadHandler} closeFn={() => this.setState({ downloadsOpen: false })} />
downloadManager={downloadHandler}
closeFn={() => this.setState({ downloadsOpen: false })}
/>
) : null ) : null
} }
@@ -219,22 +233,18 @@ class App extends React.Component<IProps, IState> {
{ {
// Game downloads menu // Game downloads menu
this.state.gameDownloadsOpen ? ( this.state.gameDownloadsOpen ? (
<Game <Game downloadManager={downloadHandler} closeFn={() => this.setState({ gameDownloadsOpen: false })} />
downloadManager={downloadHandler}
closeFn={() => this.setState({ gameDownloadsOpen: false })}
/>
) : null ) : null
} }
<div className="BottomSection" id="bottomSectionContainer"> <div className="BottomSection" id="bottomSectionContainer">
<ServerLaunchSection /> <ServerLaunchSection />
<div id="DownloadProgress" <div
id="DownloadProgress"
onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })} onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })}
> >
{ this.state.isDownloading ? {this.state.isDownloading ? <MainProgressBar downloadManager={downloadHandler} /> : null}
<MainProgressBar downloadManager={downloadHandler} />
: null }
</div> </div>
</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,14 +24,14 @@ 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')
} }
@@ -40,7 +40,7 @@ function none() {
alert('none') alert('none')
} }
class Debug extends React.Component{ class Debug extends React.Component {
render() { render() {
return ( return (
<div className="App"> <div className="App">

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> {
@@ -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}

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'

View File

@@ -109,17 +109,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

@@ -11,26 +11,26 @@ import Server from '../../resources/icons/server.svg'
import Akebi from '../../resources/icons/akebi.svg' import Akebi from '../../resources/icons/akebi.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 IState {
grasscutterEnabled: boolean; grasscutterEnabled: boolean
buttonLabel: string; buttonLabel: string
checkboxLabel: string; checkboxLabel: string
ip: string; ip: string
port: string; port: string
ipPlaceholder: string; ipPlaceholder: string
portPlaceholder: string; portPlaceholder: string
portHelpText: string; portHelpText: string
httpsLabel: string; httpsLabel: string
httpsEnabled: boolean; httpsEnabled: boolean
swag: boolean; swag: boolean
} }
export default class ServerLaunchSection extends React.Component<{}, IState> { export default class ServerLaunchSection extends React.Component<{}, IState> {
@@ -48,7 +48,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
portHelpText: '', portHelpText: '',
httpsLabel: '', httpsLabel: '',
httpsEnabled: false, httpsEnabled: false,
swag: false swag: false,
} }
this.toggleGrasscutter = this.toggleGrasscutter.bind(this) this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
@@ -74,7 +74,7 @@ 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,
}) })
} }
@@ -85,7 +85,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)
@@ -94,7 +94,7 @@ 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
} }
@@ -117,14 +117,16 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
await setConfigOption('last_port', this.state.port) await setConfigOption('last_port', this.state.port)
await invoke('enable_process_watcher', { await invoke('enable_process_watcher', {
process: proc_name || game_exe process: proc_name || game_exe,
}) })
if (config.use_internal_proxy) { if (config.use_internal_proxy) {
// Set IP // Set IP
await invoke('set_proxy_addr', { addr: (this.state.httpsEnabled ? 'https':'http') + '://' + this.state.ip + ':' + this.state.port }) await invoke('set_proxy_addr', {
addr: (this.state.httpsEnabled ? 'https' : 'http') + '://' + this.state.ip + ':' + this.state.port,
})
// Connect to proxy // Connect to proxy
await invoke('connect', { port: 8365, certificatePath: await dataDir() + '\\cultivation\\ca' }) await invoke('connect', { port: 8365, certificatePath: (await dataDir()) + '\\cultivation\\ca' })
} }
// Open server as well if the options are set // Open server as well if the options are set
@@ -137,21 +139,23 @@ 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', { path: exe || config.game_install_path })
@@ -175,7 +179,7 @@ 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 || '',
}) })
} }
@@ -194,7 +198,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
// First launch 3dm // First launch 3dm
invoke('run_program', { invoke('run_program', {
path: config.migoto_path path: config.migoto_path,
}) })
// Then play the game as normal // Then play the game as normal
@@ -203,13 +207,13 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
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,
}) })
} }
@@ -220,7 +224,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)
@@ -230,40 +234,59 @@ 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>
<> {this.state.swag && (
<BigButton onClick={this.launchAkebi} id="akebiLaunch"> <>
<img className="AkebiIcon" id="akebiIcon" src={Akebi} /> <BigButton onClick={this.launchAkebi} id="akebiLaunch">
</BigButton> <img className="AkebiIcon" id="akebiIcon" src={Akebi} />
<BigButton onClick={this.launch3dm} id="serverLaunch"> </BigButton>
3DM <BigButton onClick={this.launch3dm} id="serverLaunch">
</BigButton> 3DM
</> </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>

View File

@@ -12,15 +12,15 @@ import './TopBar.css'
import { getConfig, setConfigOption } from '../../utils/configuration' import { getConfig, setConfigOption } from '../../utils/configuration'
interface IProps { interface IProps {
optFunc: () => void; optFunc: () => void
downFunc: () => void; downFunc: () => void
gameFunc: () => 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 +30,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,7 +59,7 @@ 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)
@@ -75,7 +75,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,29 +89,31 @@ 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'> <div id="settingsBtn" onClick={this.props.optFunc} className="TopButton">
<img src={cogBtn} alt="settings" /> <img src={cogBtn} alt="settings" />
</div> </div>
<div id="downloadsBtn" className='TopButton' onClick={this.props.downFunc}> <div id="downloadsBtn" className="TopButton" onClick={this.props.downFunc}>
<img src={downBtn} alt="downloads" /> <img src={downBtn} alt="downloads" />
</div> </div>
{/* <div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}> {/* <div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>

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,7 +37,11 @@ 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,7 +17,7 @@
.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 {

View File

@@ -4,9 +4,9 @@ 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
} }
@@ -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,11 +41,9 @@ 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

@@ -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,13 @@ 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) 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 +79,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 +87,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)

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

@@ -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> {

View File

@@ -5,7 +5,7 @@ import Help from '../../../resources/icons/help.svg'
import MiniDialog from '../MiniDialog' import MiniDialog from '../MiniDialog'
interface IProps { interface IProps {
children?: React.ReactNode[] | React.ReactNode; children?: React.ReactNode[] | React.ReactNode
contents?: string contents?: string
id?: string id?: string
} }
@@ -19,7 +19,7 @@ export default class HelpButton extends React.Component<IProps, IState> {
super(props) super(props)
this.state = { this.state = {
opened: false opened: false,
} }
this.setOpen = this.setOpen.bind(this) this.setOpen = this.setOpen.bind(this)
@@ -41,12 +41,13 @@ export default class HelpButton extends React.Component<IProps, IState> {
<img src={Help} /> <img src={Help} />
</div> </div>
<div className="HelpContents" style={{ <div
display: this.state.opened ? 'block' : 'none' className="HelpContents"
}}> style={{
<MiniDialog closeFn={this.setClosed}> display: this.state.opened ? 'block' : 'none',
{this.props.contents || this.props.children} }}
</MiniDialog> >
<MiniDialog closeFn={this.setClosed}>{this.props.contents || this.props.children}</MiniDialog>
</div> </div>
</div> </div>
) )

View File

@@ -4,15 +4,15 @@ import Tr from '../../../utils/language'
import './ProgressBar.css' import './ProgressBar.css'
interface IProps { interface IProps {
downloadManager: DownloadHandler, downloadManager: DownloadHandler
} }
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 +29,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,20 +51,23 @@ 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) {
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"> <div className="MainProgressText">

View File

@@ -1,4 +1,5 @@
.ProgressBar, .InnerProgress { .ProgressBar,
.InnerProgress {
border-radius: 4px; border-radius: 4px;
} }

View File

@@ -7,14 +7,14 @@ 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,9 +85,7 @@ 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

@@ -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,13 +43,21 @@ 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()
@@ -58,10 +66,11 @@ export default class TextInput extends React.Component<IProps, IState> {
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;

View File

@@ -20,8 +20,8 @@ const DEV_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/bu
const RESOURCES_DOWNLOAD = 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/2.8/GrasscutterResources-2.8.zip' const RESOURCES_DOWNLOAD = 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/2.8/GrasscutterResources-2.8.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,7 +111,7 @@ 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', () => {
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons) unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
}) })
@@ -118,7 +120,7 @@ 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', () => {
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons) unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
}) })
@@ -127,7 +129,7 @@ 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', () => {
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons) unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
}) })
@@ -139,7 +141,7 @@ export default class Downloads extends React.Component<IProps, IState> {
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', () => {
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons) unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
}) })
@@ -153,11 +155,13 @@ export default class Downloads extends React.Component<IProps, IState> {
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',
}) })
} }
@@ -165,7 +169,7 @@ export default class Downloads extends React.Component<IProps, IState> {
// 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()
@@ -190,32 +194,40 @@ 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>
<Tr text="help.gc_stable_jar" /> <Tr text="help.gc_stable_jar" />
</HelpButton> </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>
<Tr text="help.gc_dev_jar" /> <Tr text="help.gc_dev_jar" />
</HelpButton> </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 +235,48 @@ 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
? 'downloads.grasscutter_stable_data'
: 'downloads.grasscutter_stable_data_update'
}
/>
<HelpButton> <HelpButton>
<Tr text="help.gc_stable_data" /> <Tr text="help.gc_stable_data" />
</HelpButton> </HelpButton>
</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
? 'downloads.grasscutter_latest_data'
: 'downloads.grasscutter_latest_data_update'
}
/>
<HelpButton> <HelpButton>
<Tr text="help.gc_dev_data" /> <Tr text="help.gc_dev_data" />
</HelpButton> </HelpButton>
</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 +284,19 @@ 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>
<Tr text="help.resources" /> <Tr text="help.resources" />
</HelpButton> </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>

View File

@@ -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,37 +45,49 @@ 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', () => {
unzip(folder + '\\game.zip', folder + '\\', () => { unzip(folder + '\\game.zip', folder + '\\', () => {
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>
} ) : (
<BigButton id="disabledGameBtn" onClick={() => null} disabled>
Download Game
</BigButton>
)}
<HelpButton> <HelpButton>
<Tr text="main.game_help_text" /> <Tr text="main.game_help_text" />
</HelpButton> </HelpButton>
</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

@@ -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,13 +18,15 @@ 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

@@ -16,8 +16,8 @@ import DownloadHandler from '../../../utils/download'
import * as meta from '../../../utils/metadata' import * as meta from '../../../utils/metadata'
interface IProps { interface IProps {
closeFn: () => void; closeFn: () => void
downloadManager: DownloadHandler; downloadManager: DownloadHandler
} }
interface IState { interface IState {
@@ -25,7 +25,7 @@ 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[]
@@ -61,7 +61,7 @@ export default class Options extends React.Component<IProps, IState> {
// Swag stuff // Swag stuff
akebi_path: '', akebi_path: '',
migoto_path: '' migoto_path: '',
} }
this.setGameExecutable = this.setGameExecutable.bind(this) this.setGameExecutable = this.setGameExecutable.bind(this)
@@ -136,7 +136,7 @@ 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,
}) })
} }
@@ -144,7 +144,7 @@ export default class Options extends React.Component<IProps, IState> {
setConfigOption('migoto_path', value) setConfigOption('migoto_path', value)
this.setState({ this.setState({
migoto_path: value migoto_path: value,
}) })
} }
@@ -220,7 +220,7 @@ export default class Options extends React.Component<IProps, IState> {
async installCert() { async installCert() {
await invoke('generate_ca_files', { await invoke('generate_ca_files', {
path: await dataDir() + 'cultivation' path: (await dataDir()) + 'cultivation',
}) })
} }
@@ -261,7 +261,7 @@ export default class Options extends React.Component<IProps, IState> {
</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>
@@ -270,11 +270,7 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="options.patch_metadata" /> <Tr text="options.patch_metadata" />
</div> </div>
<div className="OptionValue" id="menuOptionsCheckboxPatchMeta"> <div className="OptionValue" id="menuOptionsCheckboxPatchMeta">
<Checkbox <Checkbox onChange={this.toggleMetadata} checked={this.state?.patch_metadata} id="patchMeta" />
onChange={this.toggleMetadata}
checked={this.state?.patch_metadata}
id="patchMeta"
/>
</div> </div>
</div> </div>
<div className="OptionSection" id="menuOptionsContainerUseProxy"> <div className="OptionSection" id="menuOptionsContainerUseProxy">
@@ -282,18 +278,14 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="options.use_proxy" /> <Tr text="options.use_proxy" />
</div> </div>
<div className="OptionValue" id="menuOptionsCheckboxUseProxy"> <div className="OptionValue" id="menuOptionsCheckboxUseProxy">
<Checkbox <Checkbox onChange={this.toggleProxy} checked={this.state?.use_internal_proxy} id="useProxy" />
onChange={this.toggleProxy}
checked={this.state?.use_internal_proxy}
id="useProxy"
/>
</div> </div>
</div> </div>
<Divider /> <Divider />
<div className='OptionSection' id="menuOptionsContainerGCJar"> <div className="OptionSection" id="menuOptionsContainerGCJar">
<div className='OptionLabel' id="menuOptionsLabelGCJar"> <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">
@@ -310,39 +302,34 @@ 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='OptionSection' id="menuOptionsContainerMigoto"> <div className="OptionSection" id="menuOptionsContainerMigoto">
<div className='OptionLabel' id="menuOptionsLabelMigoto"> <div className="OptionLabel" id="menuOptionsLabelMigoto">
<Tr text="swag.migoto" /> <Tr text="swag.migoto" />
</div> </div>
<div className='OptionValue' id="menuOptionsDirMigoto"> <div className="OptionValue" id="menuOptionsDirMigoto">
<DirInput onChange={this.setMigoto} value={this.state?.migoto_path} extensions={['exe']} /> <DirInput onChange={this.setMigoto} value={this.state?.migoto_path} extensions={['exe']} />
</div> </div>
</div> </div>
</> </div>
) </>
} )}
<Divider /> <Divider />

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
@@ -86,15 +88,19 @@ export default class NewsSection extends React.Component<IProps, IState> {
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,17 +145,23 @@ 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>
) )

View File

@@ -3,8 +3,7 @@ import { dataDir } from '@tauri-apps/api/path'
let configFilePath: string let configFilePath: string
let defaultConfig: Configuration let defaultConfig: Configuration
;(async () => {
(async() => {
defaultConfig = { defaultConfig = {
toggle_grasscutter: false, toggle_grasscutter: false,
game_install_path: 'C:\\Program Files\\Genshin Impact\\Genshin Impact game\\GenshinImpact.exe', game_install_path: 'C:\\Program Files\\Genshin Impact\\Genshin Impact game\\GenshinImpact.exe',
@@ -58,7 +57,7 @@ export async function setConfigOption<K extends keyof Configuration>(key: K, val
const config = await getConfig() const config = await getConfig()
config[key] = value config[key] = value
await saveConfig(<Configuration> config) await saveConfig(<Configuration>config)
} }
export async function getConfigOption<K extends keyof Configuration>(key: K): Promise<Configuration[K]> { export async function getConfigOption<K extends keyof Configuration>(key: K): Promise<Configuration[K]> {
@@ -73,8 +72,8 @@ export async function getConfig() {
let parsed: Configuration = defaultConfig let parsed: Configuration = defaultConfig
try { try {
parsed = <Configuration> JSON.parse(raw) parsed = <Configuration>JSON.parse(raw)
} catch(e) { } catch (e) {
// We could not open the file // We could not open the file
console.log(e) console.log(e)
@@ -100,7 +99,7 @@ async function readConfigFile() {
if (!dirs.find((fileOrDir) => fileOrDir?.name === 'cultivation')) { if (!dirs.find((fileOrDir) => fileOrDir?.name === 'cultivation')) {
// Create dir // Create dir
await fs.createDir(local + 'cultivation').catch(e => console.log(e)) await fs.createDir(local + 'cultivation').catch((e) => console.log(e))
} }
const innerDirs = await fs.readDir(local + '/cultivation') const innerDirs = await fs.readDir(local + '/cultivation')
@@ -108,7 +107,7 @@ async function readConfigFile() {
// Create grasscutter dir for potential installation // Create grasscutter dir for potential installation
if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) { if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) {
// Create dir // Create dir
await fs.createDir(local + 'cultivation/grasscutter').catch(e => console.log(e)) await fs.createDir(local + 'cultivation/grasscutter').catch((e) => console.log(e))
} }
const dataFiles = await fs.readDir(local + 'cultivation') const dataFiles = await fs.readDir(local + 'cultivation')
@@ -118,7 +117,7 @@ async function readConfigFile() {
// Create config file // Create config file
const file: fs.FsTextFileOption = { const file: fs.FsTextFileOption = {
path: configFilePath, path: configFilePath,
contents: JSON.stringify(defaultConfig) contents: JSON.stringify(defaultConfig),
} }
await fs.writeFile(file) await fs.writeFile(file)
@@ -132,6 +131,6 @@ async function writeConfigFile(raw: string) {
// All external config functions call readConfigFile, which ensure files exists // All external config functions call readConfigFile, which ensure files exists
await fs.writeFile({ await fs.writeFile({
path: configFilePath, path: configFilePath,
contents: raw contents: raw,
}) })
} }

View File

@@ -4,15 +4,15 @@ import { byteToString } from './string'
export default class DownloadHandler { export default class DownloadHandler {
downloads: { downloads: {
path: string, path: string
progress: number, progress: number
total: number, total: number
total_downloaded: number, total_downloaded: number
status: string, status: string
startTime: number, startTime: number
error?: string, error?: string
speed?: string, speed?: string
onFinish?: () => void, onFinish?: () => void
}[] }[]
// Pass tauri invoke function // Pass tauri invoke function
@@ -22,13 +22,13 @@ export default class DownloadHandler {
listen('download_progress', ({ payload }) => { listen('download_progress', ({ payload }) => {
// @ts-expect-error Payload may be unknown but backend always returns this object // @ts-expect-error Payload may be unknown but backend always returns this object
const obj: { const obj: {
downloaded: string, downloaded: string
total: string, total: string
path: string, path: string
total_downloaded: string, total_downloaded: string
} = payload } = payload
const index = this.downloads.findIndex(download => download.path === obj.path) const index = this.downloads.findIndex((download) => download.path === obj.path)
this.downloads[index].progress = parseInt(obj.downloaded, 10) this.downloads[index].progress = parseInt(obj.downloaded, 10)
this.downloads[index].total = parseInt(obj.total, 10) this.downloads[index].total = parseInt(obj.total, 10)
this.downloads[index].total_downloaded = parseInt(obj.total_downloaded, 10) this.downloads[index].total_downloaded = parseInt(obj.total_downloaded, 10)
@@ -52,7 +52,7 @@ export default class DownloadHandler {
const filename = payload const filename = payload
// set status to finished // set status to finished
const index = this.downloads.findIndex(download => download.path === filename) const index = this.downloads.findIndex((download) => download.path === filename)
this.downloads[index].status = 'finished' this.downloads[index].status = 'finished'
// Call onFinish callback // Call onFinish callback
@@ -65,12 +65,12 @@ export default class DownloadHandler {
listen('download_error', ({ payload }) => { listen('download_error', ({ payload }) => {
// @ts-expect-error shut up typescript // @ts-expect-error shut up typescript
const errorData: { const errorData: {
path: string, path: string
error: string, error: string
} = payload } = payload
// Set download to error // Set download to error
const index = this.downloads.findIndex(download => download.path === errorData.path) const index = this.downloads.findIndex((download) => download.path === errorData.path)
this.downloads[index].status = 'error' this.downloads[index].status = 'error'
this.downloads[index].error = errorData.error this.downloads[index].error = errorData.error
}) })
@@ -78,13 +78,13 @@ export default class DownloadHandler {
// Extraction events // Extraction events
listen('extract_start', ({ payload }) => { listen('extract_start', ({ payload }) => {
// Find the download that is no extracting and set it's status as such // Find the download that is no extracting and set it's status as such
const index = this.downloads.findIndex(download => download.path === payload) const index = this.downloads.findIndex((download) => download.path === payload)
this.downloads[index].status = 'extracting' this.downloads[index].status = 'extracting'
}) })
listen('extract_end', ({ payload }) => { listen('extract_end', ({ payload }) => {
// Find the download that is no extracting and set it's status as such // Find the download that is no extracting and set it's status as such
const index = this.downloads.findIndex(download => download.path === payload) const index = this.downloads.findIndex((download) => download.path === payload)
this.downloads[index].status = 'finished' this.downloads[index].status = 'finished'
}) })
} }
@@ -95,16 +95,16 @@ export default class DownloadHandler {
downloadingJar() { downloadingJar() {
// Kinda hacky but it works // Kinda hacky but it works
return this.downloads.some(d => d.path.includes('grasscutter.zip')) return this.downloads.some((d) => d.path.includes('grasscutter.zip'))
} }
downloadingResources() { downloadingResources() {
// Kinda hacky but it works // Kinda hacky but it works
return this.downloads.some(d => d.path.includes('resources')) return this.downloads.some((d) => d.path.includes('resources'))
} }
downloadingRepo() { downloadingRepo() {
return this.downloads.some(d => d.path.includes('grasscutter_repo.zip')) return this.downloads.some((d) => d.path.includes('grasscutter_repo.zip'))
} }
addDownload(url: string, path: string, onFinish?: () => void) { addDownload(url: string, path: string, onFinish?: () => void) {
@@ -128,24 +128,24 @@ export default class DownloadHandler {
invoke('stop_download', { path }) invoke('stop_download', { path })
// Remove from list // Remove from list
const index = this.downloads.findIndex(download => download.path === path) const index = this.downloads.findIndex((download) => download.path === path)
this.downloads.splice(index, 1) this.downloads.splice(index, 1)
} }
getDownloadProgress(path: string) { getDownloadProgress(path: string) {
const index = this.downloads.findIndex(download => download.path === path) const index = this.downloads.findIndex((download) => download.path === path)
return this.downloads[index] || null return this.downloads[index] || null
} }
getDownloadSize(path: string) { getDownloadSize(path: string) {
const index = this.downloads.findIndex(download => download.path === path) const index = this.downloads.findIndex((download) => download.path === path)
return byteToString(this.downloads[index].total) || null return byteToString(this.downloads[index].total) || null
} }
getTotalAverage() { getTotalAverage() {
const files = this.downloads.filter(d => d.status === 'downloading') const files = this.downloads.filter((d) => d.status === 'downloading')
const total = files.reduce((acc, d) => acc + d.total, 0) const total = files.reduce((acc, d) => acc + d.total, 0)
const progress = files.reduce((acc, d) => d.progress !== 0 ? acc + d.progress : acc + d.total_downloaded, 0) const progress = files.reduce((acc, d) => (d.progress !== 0 ? acc + d.progress : acc + d.total_downloaded), 0)
let speedStr = '0 B/s' let speedStr = '0 B/s'
// Get download speed based on startTimes // Get download speed based on startTimes
@@ -158,10 +158,10 @@ export default class DownloadHandler {
return { return {
average: (progress / total) * 100 || 0, average: (progress / total) * 100 || 0,
files: this.downloads.filter(d => d.status === 'downloading').length, files: this.downloads.filter((d) => d.status === 'downloading').length,
extracting: this.downloads.filter(d => d.status === 'extracting').length, extracting: this.downloads.filter((d) => d.status === 'extracting').length,
totalSize: total, totalSize: total,
speed: speedStr speed: speedStr,
} }
} }
} }

View File

@@ -3,7 +3,7 @@ import { getConfig } from './configuration'
export async function getGameExecutable() { export async function getGameExecutable() {
const config = await getConfig() const config = await getConfig()
if(!config.game_install_path) { if (!config.game_install_path) {
return null return null
} }
@@ -14,7 +14,7 @@ export async function getGameExecutable() {
export async function getGameFolder() { export async function getGameFolder() {
const config = await getConfig() const config = await getConfig()
if(!config.game_install_path) { if (!config.game_install_path) {
return null return null
} }

View File

@@ -3,12 +3,12 @@ import React from 'react'
import { getConfigOption } from './configuration' import { getConfigOption } from './configuration'
interface IProps { interface IProps {
text: string; text: string
} }
interface IState { interface IState {
language: string; language: string
translated_text: string; translated_text: string
} }
export default class Tr extends React.Component<IProps, IState> { export default class Tr extends React.Component<IProps, IState> {
@@ -28,7 +28,7 @@ export default class Tr extends React.Component<IProps, IState> {
if (!language) language = 'en' if (!language) language = 'en'
invoke('get_lang', { lang: language }).then((response) => { invoke('get_lang', { lang: language }).then((response) => {
const translation_obj = JSON.parse(response as string || '{}') const translation_obj = JSON.parse((response as string) || '{}')
// Traversal // Traversal
if (text.includes('.')) { if (text.includes('.')) {
@@ -39,7 +39,7 @@ export default class Tr extends React.Component<IProps, IState> {
if (!translation) { if (!translation) {
translation = '' translation = ''
} else { } else {
translation = typeof translation !== 'string' ? translation[keys[i]] : translation as string translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
} }
} }
@@ -48,7 +48,7 @@ export default class Tr extends React.Component<IProps, IState> {
}) })
} else { } else {
this.setState({ this.setState({
translated_text: translation_obj[text] || '' translated_text: translation_obj[text] || '',
}) })
} }
}) })
@@ -62,13 +62,13 @@ export default class Tr extends React.Component<IProps, IState> {
export async function getLanguages() { export async function getLanguages() {
const resp: { const resp: {
[key: string]: string; [key: string]: string
} = await invoke('get_languages') } = await invoke('get_languages')
const lang_list: { const lang_list: {
[key: string]: string; [key: string]: string
}[] = [] }[] = []
Object.keys(resp).forEach(k => { Object.keys(resp).forEach((k) => {
const parsed = JSON.parse(resp[k]) const parsed = JSON.parse(resp[k])
if (parsed.lang_name) { if (parsed.lang_name) {
@@ -80,8 +80,8 @@ export async function getLanguages() {
} }
export async function translate(text: string) { export async function translate(text: string) {
const language = await getConfigOption('language') || 'en' const language = (await getConfigOption('language')) || 'en'
const translation_json = JSON.parse(await invoke('get_lang', { lang: language }) || '{}') const translation_json = JSON.parse((await invoke('get_lang', { lang: language })) || '{}')
// Traversal // Traversal
if (text.includes('.')) { if (text.includes('.')) {
@@ -92,7 +92,7 @@ export async function translate(text: string) {
if (!translation) { if (!translation) {
translation = '' translation = ''
} else { } else {
translation = typeof translation !== 'string' ? translation[keys[i]] : translation as string translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
} }
} }

View File

@@ -5,7 +5,7 @@ import { getGameExecutable, getGameFolder } from './game'
export async function patchMetadata() { export async function patchMetadata() {
const metadataExists = await invoke('dir_exists', { const metadataExists = await invoke('dir_exists', {
path: await getGameMetadataPath() + '\\global-metadata.dat' path: (await getGameMetadataPath()) + '\\global-metadata.dat',
}) })
if (!metadataExists) { if (!metadataExists) {
@@ -16,9 +16,9 @@ export async function patchMetadata() {
// Copy unpatched metadata to backup location // Copy unpatched metadata to backup location
const copiedMeta = await invoke('copy_file_with_new_name', { const copiedMeta = await invoke('copy_file_with_new_name', {
path: await getGameMetadataPath() + '\\global-metadata.dat', path: (await getGameMetadataPath()) + '\\global-metadata.dat',
newPath: await getBackupMetadataPath(), newPath: await getBackupMetadataPath(),
newName: 'global-metadata-unpatched.dat' newName: 'global-metadata-unpatched.dat',
}) })
if (!copiedMeta) { if (!copiedMeta) {
@@ -42,9 +42,9 @@ export async function patchMetadata() {
console.log('Replacing unpatched game metadata with patched metadata') console.log('Replacing unpatched game metadata with patched metadata')
const replacedMeta = await invoke('copy_file_with_new_name', { const replacedMeta = await invoke('copy_file_with_new_name', {
path: await getBackupMetadataPath() + '\\global-metadata-patched.dat', path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
newPath: await getGameMetadataPath(), newPath: await getGameMetadataPath(),
newName: 'global-metadata.dat' newName: 'global-metadata.dat',
}) })
if (!replacedMeta) { if (!replacedMeta) {
@@ -58,7 +58,7 @@ export async function patchMetadata() {
export async function patchGame() { export async function patchGame() {
const backupExists = await invoke('dir_exists', { const backupExists = await invoke('dir_exists', {
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat' path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
}) })
if (!backupExists) { if (!backupExists) {
@@ -72,7 +72,7 @@ export async function patchGame() {
// Do we have a patch already? // Do we have a patch already?
const patchedExists = await invoke('dir_exists', { const patchedExists = await invoke('dir_exists', {
path: await getBackupMetadataPath() + '\\global-metadata-patched.dat' path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
}) })
if (!patchedExists) { if (!patchedExists) {
@@ -86,8 +86,8 @@ export async function patchGame() {
// Are we already patched? If so, that's fine, just continue as normal // Are we already patched? If so, that's fine, just continue as normal
const gameIsPatched = await invoke('are_files_identical', { const gameIsPatched = await invoke('are_files_identical', {
path1: await getBackupMetadataPath() + '\\global-metadata-patched.dat', path1: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
path2: await getGameMetadataPath() + '\\global-metadata.dat' path2: (await getGameMetadataPath()) + '\\global-metadata.dat',
}) })
if (gameIsPatched) { if (gameIsPatched) {
@@ -96,17 +96,17 @@ export async function patchGame() {
// Is the current backup the same as the games current metadata? // Is the current backup the same as the games current metadata?
const backupIsCurrent = await invoke('are_files_identical', { const backupIsCurrent = await invoke('are_files_identical', {
path1: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat', path1: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
path2: await getGameMetadataPath() + '\\global-metadata.dat' path2: (await getGameMetadataPath()) + '\\global-metadata.dat',
}) })
// Game has probably been updated. We need to repatch the game... // Game has probably been updated. We need to repatch the game...
if (!backupIsCurrent) { if (!backupIsCurrent) {
const deletedOldBackup = await invoke('delete_file', { const deletedOldBackup = await invoke('delete_file', {
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat' path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
}) })
const deletedOldPatched = await invoke('delete_file', { const deletedOldPatched = await invoke('delete_file', {
path: await getBackupMetadataPath() + '\\global-metadata-patched.dat' path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
}) })
// It's fine if these deletes fail. The game will be replaced anyway. // It's fine if these deletes fail. The game will be replaced anyway.
@@ -134,9 +134,9 @@ export async function patchGame() {
// Finally, replace the unpatched metadata with the patched one // Finally, replace the unpatched metadata with the patched one
const replaced = await invoke('copy_file_with_new_name', { const replaced = await invoke('copy_file_with_new_name', {
path: await getBackupMetadataPath() + '\\global-metadata-patched.dat', path: (await getBackupMetadataPath()) + '\\global-metadata-patched.dat',
newPath: await getGameMetadataPath(), newPath: await getGameMetadataPath(),
newName: 'global-metadata.dat' newName: 'global-metadata.dat',
}) })
if (!replaced) { if (!replaced) {
@@ -148,7 +148,7 @@ export async function patchGame() {
export async function unpatchGame() { export async function unpatchGame() {
const backupExists = await invoke('dir_exists', { const backupExists = await invoke('dir_exists', {
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat' path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
}) })
if (!backupExists) { if (!backupExists) {
@@ -157,9 +157,9 @@ export async function unpatchGame() {
} }
const replaced = await invoke('copy_file_with_new_name', { const replaced = await invoke('copy_file_with_new_name', {
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat', path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
newPath: await getGameMetadataPath(), newPath: await getGameMetadataPath(),
newName: 'global-metadata.dat' newName: 'global-metadata.dat',
}) })
return replaced return replaced
@@ -172,20 +172,26 @@ export async function getGameMetadataPath() {
return null return null
} }
return (await getGameFolder() + '\\' + gameExec.replace('.exe', '_Data') + '\\Managed\\Metadata').replace(/\\/g, '/') return ((await getGameFolder()) + '\\' + gameExec.replace('.exe', '_Data') + '\\Managed\\Metadata').replace(
/\\/g,
'/'
)
} }
export async function getBackupMetadataPath() { export async function getBackupMetadataPath() {
return await dataDir() + 'cultivation\\metadata' return (await dataDir()) + 'cultivation\\metadata'
} }
export async function globalMetadataLink() { export async function globalMetadataLink() {
const versionAPIUrl = 'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10&sub_channel_id=0' const versionAPIUrl =
'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10&sub_channel_id=0'
// Get versions from API // Get versions from API
const versions = JSON.parse(await invoke('web_get', { const versions = JSON.parse(
url: versionAPIUrl await invoke('web_get', {
})) url: versionAPIUrl,
})
)
if (!versions || versions.retcode !== 0) { if (!versions || versions.retcode !== 0) {
console.log('Failed to get versions from API') console.log('Failed to get versions from API')
@@ -195,7 +201,7 @@ export async function globalMetadataLink() {
// Get latest version // Get latest version
const latest = versions.data.game.latest const latest = versions.data.game.latest
return latest.decompressed_path as string + '/GenshinImpact_Data/Managed/Metadata/global-metadata.dat' return (latest.decompressed_path as string) + '/GenshinImpact_Data/Managed/Metadata/global-metadata.dat'
} }
export async function restoreMetadata(manager: DownloadHandler) { export async function restoreMetadata(manager: DownloadHandler) {
@@ -208,16 +214,16 @@ export async function restoreMetadata(manager: DownloadHandler) {
// Should make sure metadata path exists since the user may have deleted it // Should make sure metadata path exists since the user may have deleted it
await invoke('dir_create', { await invoke('dir_create', {
path: await getBackupMetadataPath() path: await getBackupMetadataPath(),
}) })
// It is possible the unpatched backup is mistakenly patched // It is possible the unpatched backup is mistakenly patched
await invoke('delete_file', { await invoke('delete_file', {
path: await getBackupMetadataPath() + '\\global-metadata-unpatched.dat' path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
}) })
// Download the file // Download the file
manager.addDownload(metaLink, await getBackupMetadataPath() + '\\global-metadata-unpatched.dat', () => { manager.addDownload(metaLink, (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat', () => {
unpatchGame() unpatchGame()
}) })
console.log('Restoring backedup metadata') console.log('Restoring backedup metadata')

View File

@@ -4,10 +4,12 @@ export async function toggleEncryption(path: string) {
let serverConf let serverConf
try { try {
serverConf = JSON.parse(await invoke('read_file', { serverConf = JSON.parse(
path, await invoke('read_file', {
})) path,
} catch(e) { })
)
} catch (e) {
console.log(`Server config at ${path} not found or invalid`) console.log(`Server config at ${path} not found or invalid`)
return return
} }
@@ -28,10 +30,12 @@ export async function encryptionEnabled(path: string) {
let serverConf let serverConf
try { try {
serverConf = JSON.parse(await invoke('read_file', { serverConf = JSON.parse(
path, await invoke('read_file', {
})) path,
} catch(e) { })
)
} catch (e) {
console.log(`Server config at ${path} not found or invalid`) console.log(`Server config at ${path} not found or invalid`)
return false return false
} }

View File

@@ -33,15 +33,15 @@ const defaultTheme = {
description: 'Default theme', description: 'Default theme',
includes: { includes: {
css: [], css: [],
js: [] js: [],
}, },
path: 'default' path: 'default',
} }
export async function getThemeList() { export async function getThemeList() {
// Do some invoke to backend to get the theme list // Do some invoke to backend to get the theme list
const themes = await invoke('get_theme_list', { const themes = (await invoke('get_theme_list', {
dataDir: `${await dataDir()}/cultivation` dataDir: `${await dataDir()}/cultivation`,
}) as BackendThemeList[] })) as BackendThemeList[]
const list: ThemeList[] = [ const list: ThemeList[] = [
// ALWAYS include default theme // ALWAYS include default theme
{ {
@@ -50,13 +50,13 @@ export async function getThemeList() {
description: 'Default theme', description: 'Default theme',
includes: { includes: {
css: [], css: [],
js: [] js: [],
}, },
path: 'default' path: 'default',
} },
] ]
themes.forEach(t => { themes.forEach((t) => {
let obj let obj
try { try {
@@ -74,7 +74,7 @@ export async function getThemeList() {
export async function getTheme(name: string) { export async function getTheme(name: string) {
const themes = await getThemeList() const themes = await getThemeList()
return themes.find(t => t.name === name) || defaultTheme return themes.find((t) => t.name === name) || defaultTheme
} }
export async function loadTheme(theme: ThemeList, document: Document) { export async function loadTheme(theme: ThemeList, document: Document) {
@@ -89,7 +89,7 @@ export async function loadTheme(theme: ThemeList, document: Document) {
const jsIncludes = theme.includes.js const jsIncludes = theme.includes.js
// Load CSS files // Load CSS files
cssIncludes.forEach(css => { cssIncludes.forEach((css) => {
if (!css) return if (!css) return
const link = document.createElement('link') const link = document.createElement('link')
@@ -100,7 +100,7 @@ export async function loadTheme(theme: ThemeList, document: Document) {
}) })
// Load JS files // Load JS files
jsIncludes.forEach(js => { jsIncludes.forEach((js) => {
if (!js) return if (!js) return
const script = document.createElement('script') const script = document.createElement('script')
@@ -125,7 +125,7 @@ export async function loadTheme(theme: ThemeList, document: Document) {
// Save the background to our data dir // Save the background to our data dir
await invoke('copy_file', { await invoke('copy_file', {
path: theme.path + '/' + theme.customBackgroundPath, path: theme.path + '/' + theme.customBackgroundPath,
newPath: bgPath newPath: bgPath,
}) })
// Set the background // Set the background

View File

@@ -7,7 +7,7 @@ export function unzip(file: string, dest: string, onFinish?: () => void) {
destpath: dest, destpath: dest,
}) })
listen('extract_end', ({payload}) => { listen('extract_end', ({ payload }) => {
if (payload === file && onFinish) { if (payload === file && onFinish) {
onFinish() onFinish()
} }

View File

@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2020", "target": "es2020",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@@ -20,7 +16,5 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src"]
"src"
]
} }