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

@ -4,12 +4,7 @@
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
@ -18,10 +13,7 @@
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"plugins": ["react", "@typescript-eslint"],
"rules": {
"@typescript-eslint/ban-types": [
"warn",

View File

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

View File

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

View File

@ -1,25 +1,28 @@
# Client Patching Notice
For game versions 2.8 and above, Cultivation automatically makes a small patch to your game client when launching using Grasscutter, and restores it upon closing the game. In theory, you should still be totally safe, however it would be dishonest to not explicitly state that **modifying the game client could, theoretically, lead to a ban if you connect to official servers with it**. It is extremely unlikely AND there are no instances known of it happening, but the possibility exists.
# Cultivation
A game launcher designed to easily proxy traffic from anime game to private servers.
While the Cultivation repository is **open**. This does **not** mean it has released.
Please do **NOT install, download, or use pre-compiled versions of Cultivation found elsewhere**. Only use releases from this GitHub repository.
# Table Of Contents
* [Download](#download)
* [Developer Quick-start](#developer-quickstart)
* [Setup](#setup)
* [Building](#building)
* [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
[Find release builds here!](https://github.com/Grasscutters/Cultivation/releases)
Once downloaded, extract somewhere and open as administrator.
@ -27,27 +30,33 @@ Once downloaded, extract somewhere and open as administrator.
# Developer Quickstart
### Setup
* Install [NodeJS >12](https://nodejs.org/en/)
* Install [yarn](https://classic.yarnpkg.com/lang/en/docs/install) (cry about it `npm` lovers)
* Install [Rust](https://www.rust-lang.org/tools/install)
* `yarn install`
* `yarn start:dev`
- Install [NodeJS >12](https://nodejs.org/en/)
- Install [yarn](https://classic.yarnpkg.com/lang/en/docs/install) (cry about it `npm` lovers)
- Install [Rust](https://www.rust-lang.org/tools/install)
- `yarn install`
- `yarn start:dev`
### Building
For a release build,
- `yarn build`
For a debug build,
- `yarn build --debug`
### Code Formatting and Linting
- `yarn format`
- `yarn lint`
### Generating Update Artifacts
* Add the `TAURI_PRIVATE_KEY` as an environment variable with a path to your private key.
* Add the `TAURI_KEY_PASSWORD` as an environment variable with the password for your private key.
* `yarn build`
- Add the `TAURI_PRIVATE_KEY` as an environment variable with a path to your private key.
- Add the `TAURI_KEY_PASSWORD` as an environment variable with the password for your private key.
- `yarn build`
The update will be at `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x64_xx-XX.msi.zip`
@ -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)
# Screenshots
![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/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)
## Credits
* [SpikeHD](https://github.com/SpikeHD): For originally creating **GrassClipper** and creating the amazing UI of Cultivation.
* [KingRainbow44](https://github.com/KingRainbow44): For building a proxy daemon from scratch and integrating it with Cultivation.
* [Benj](https://github.com/4Benj): For assistance in client patching.
* [lilmayofuksu](https://github.com/lilmayofuksu): For assistance in client patching.
* [Tauri](https://tauri.app): For providing an amazing, efficient, and simple desktop application framework/library.
- [SpikeHD](https://github.com/SpikeHD): For originally creating **GrassClipper** and creating the amazing UI of Cultivation.
- [KingRainbow44](https://github.com/KingRainbow44): For building a proxy daemon from scratch and integrating it with Cultivation.
- [Benj](https://github.com/4Benj): For assistance in client patching.
- [lilmayofuksu](https://github.com/lilmayofuksu): For assistance in client patching.
- [Tauri](https://tauri.app): For providing an amazing, efficient, and simple desktop application framework/library.

View File

@ -2,7 +2,7 @@
1. Download your favorite theme! (You can find some in the `#themes` channel on Discord)
2. Place the unzipped theme folder inside of `%appdata%/cultivation/themes` (The path should look something like this: `cultivation/themes/theme_name/index.json`)
4. Enable within Cultivation!
3. Enable within Cultivation!
# Creating your own theme
@ -17,7 +17,7 @@ You will need CSS and JS experience if you want to do anything cool.
`index.json` is where you tell Cultivation which files and images to include. It supports the following properties:
| Property | Description |
| :--- | :--- |
| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | The name of the theme. |
| `version` | Not shown anywhere, the version of the theme. |
| `description` | Not shown anywhere, the description of the theme. |
@ -55,15 +55,17 @@ Below are some small examples of what you can do:
```css
/* Change the font */
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
}
```
```css
/* Remove the news section */
.NewsSection {
display: none;
}
```
```css
/* Change the right bar width */
.RightBar {
@ -72,6 +74,7 @@ body {
```
## How can I change XYZ element?
Every element is documented and describe [here](/docs/elementIds.md). Every\* single DOM element is assigned an ID to allow for easy and hyper-specific editing.
## Writing your JS
@ -83,24 +86,26 @@ Below are some examples of what you can do:
```js
/* Change the version number every 500ms */
setInterval(() => {
document.getElementById("version").innerHTML = "v" + Math.floor(Math.random() * 100);
}, 500);
document.getElementById('version').innerHTML = 'v' + Math.floor(Math.random() * 100)
}, 500)
```
```js
/* Load a custom font */
const head = document.head
const link = document.createElement("link")
const link = document.createElement('link')
link.href = "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"
link.rel = "stylesheet"
link.type = "text/css"
link.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'
link.rel = 'stylesheet'
link.type = 'text/css'
head.appendChild(link)
```
```js
/* Create a new button that does nothing */
const newButton = document.createElement("button");
newButton.innerHTML = "New Button";
const newButton = document.createElement('button')
newButton.innerHTML = 'New Button'
document.body.appendChild(newButton);
document.body.appendChild(newButton)
```

View File

@ -1,10 +1,11 @@
# Documentation of Element ID's and Classes for custom theming
## IDs
This does not include commonly used components (buttons, divider lines, commit author and message, etc...) for accessing and modifying those elements, please check `Classes` section bellow.
| #ID | Description |
|----------------------------------------|-----------------------------------------------------------------|
| ------------------------------------ | --------------------------------------------------------------- |
| `#miniDialogContainer` | Main container of MiniDialog |
| `#miniDialogContainerTop` | Affects only top section of MiniDialog |
| `#miniDialogButtonClose` | Close button (SVG) of MiniDialog |
@ -97,10 +98,11 @@ This does not include commonly used components (buttons, divider lines, commit a
| `#miniDownloadContainer` | Container for mini download |
## Classes
This is not full list of all classes, rather its list of classes for commonly used components that can not be accessed using element id system.
| .Class | Description |
|-----------------------------|---------------------------------------------------------|
| ------------------------- | ------------------------------------------------------- | --- |
| `.BigButton` | Class for all buttons |
| `.BigButtonText` | Text inside a button | |
| `.Checkbox` | Checkbox container |

View File

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

View File

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

View File

@ -58,4 +58,4 @@
"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"
}
}
}

View File

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

View File

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

View File

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

View File

@ -22,7 +22,8 @@ select:focus {
border-bottom-color: #ffd326;
}
#root, .App {
#root,
.App {
height: 100%;
}
@ -96,13 +97,13 @@ select:focus {
padding: 0;
}
@media(max-height: 580px) {
@media (max-height: 580px) {
.BottomSection {
height: 150px;
}
}
@media(max-height: 500px) {
@media (max-height: 500px) {
.BottomSection {
height: 140px;
}

View File

@ -25,16 +25,16 @@ import { getTheme, loadTheme } from '../utils/themes'
import { unpatchGame } from '../utils/metadata'
interface IProps {
[key: string]: never;
[key: string]: never
}
interface IState {
isDownloading: boolean;
optionsOpen: boolean;
miniDownloadsOpen: boolean;
downloadsOpen: boolean;
gameDownloadsOpen: boolean;
bgFile: string;
isDownloading: boolean
optionsOpen: boolean
miniDownloadsOpen: boolean
downloadsOpen: boolean
gameDownloadsOpen: boolean
bgFile: string
}
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
@ -57,7 +57,7 @@ class App extends React.Component<IProps, IState> {
console.log(payload)
})
listen('jar_extracted', ({ payload }: { payload: string}) => {
listen('jar_extracted', ({ payload }: { payload: string }) => {
setConfigOption('grasscutter_path', payload)
})
@ -71,7 +71,9 @@ class App extends React.Component<IProps, IState> {
console.log(`unpatched game? ${unpatched}`)
if (!unpatched) {
alert(`Could not unpatch game! (You should be able to find your metadata backup in ${await dataDir()}\\cultivation\\)`)
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 !!
const custom_bg = await getConfigOption('customBackground')
if(!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) {
if(game_path) {
if (!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) {
if (game_path) {
// Get the bg by invoking, then set the background to that bg.
const bgLoc: string = await invoke('get_bg_file', {
bgPath: root_path,
appdata: await dataDir()
appdata: await dataDir(),
})
bgLoc && this.setState({
bgFile: bgLoc
}, this.forceUpdate)
bgLoc &&
this.setState(
{
bgFile: bgLoc,
},
this.forceUpdate
)
}
} else {
const isUrl = /^http(s)?:\/\//gm.test(custom_bg)
if (!isUrl) {
const isValid = await invoke('dir_exists', {
path: custom_bg
path: custom_bg,
})
this.setState({
bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG
}, this.forceUpdate)
this.setState(
{
bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG,
},
this.forceUpdate
)
} else {
// Check if URL returns a valid image.
const isValid = await invoke('valid_url', {
url: custom_bg
url: custom_bg,
})
this.setState({
bgFile: isValid ? custom_bg : DEFAULT_BG
}, this.forceUpdate)
this.setState(
{
bgFile: isValid ? custom_bg : DEFAULT_BG,
},
this.forceUpdate
)
}
}
if (!cert_generated) {
// Generate the certificate
await invoke('generate_ca_files', {
path: await dataDir() + 'cultivation'
path: (await dataDir()) + 'cultivation',
})
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
setInterval(() => {
this.setState({
isDownloading: downloadHandler.getDownloads().filter(d => d.status !== 'finished')?.length > 0
isDownloading: downloadHandler.getDownloads().filter((d) => d.status !== 'finished')?.length > 0,
})
}, 1000)
}
render() {
return (
<div className="App" style={
this.state.bgFile ? {
<div
className="App"
style={
this.state.bgFile
? {
background: `url("${this.state.bgFile}") fixed`,
} : {}
}>
}
: {}
}
>
<TopBar
optFunc={() => {
this.setState({ optionsOpen: !this.state.optionsOpen })
@ -199,10 +216,7 @@ class App extends React.Component<IProps, IState> {
{
// Download menu
this.state.downloadsOpen ? (
<Downloads
downloadManager={downloadHandler}
closeFn={() => this.setState({ downloadsOpen: false })}
/>
<Downloads downloadManager={downloadHandler} closeFn={() => this.setState({ downloadsOpen: false })} />
) : null
}
@ -219,22 +233,18 @@ class App extends React.Component<IProps, IState> {
{
// Game downloads menu
this.state.gameDownloadsOpen ? (
<Game
downloadManager={downloadHandler}
closeFn={() => this.setState({ gameDownloadsOpen: false })}
/>
<Game downloadManager={downloadHandler} closeFn={() => this.setState({ gameDownloadsOpen: false })} />
) : null
}
<div className="BottomSection" id="bottomSectionContainer">
<ServerLaunchSection />
<div id="DownloadProgress"
<div
id="DownloadProgress"
onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })}
>
{ this.state.isDownloading ?
<MainProgressBar downloadManager={downloadHandler} />
: null }
{this.state.isDownloading ? <MainProgressBar downloadManager={downloadHandler} /> : null}
</div>
</div>
</div>

View File

@ -3,8 +3,8 @@ import './App.css'
import TopBar from './components/TopBar'
import {invoke} from '@tauri-apps/api/tauri'
import {dataDir} from '@tauri-apps/api/path'
import { invoke } from '@tauri-apps/api/tauri'
import { dataDir } from '@tauri-apps/api/path'
import TextInput from './components/common/TextInput'
let proxyAddress = ''
@ -15,7 +15,7 @@ async function setProxyAddress(address: string) {
}
async function startProxy() {
await invoke('connect', { port: 2222, certificatePath: await dataDir() + '\\cultivation\\ca' })
await invoke('connect', { port: 2222, certificatePath: (await dataDir()) + '\\cultivation\\ca' })
await invoke('open_in_browser', { url: 'https://hoyoverse.com' })
}
@ -24,14 +24,14 @@ async function stopProxy() {
}
async function generateCertificates() {
await invoke('generate_ca_files', { path: await dataDir() + '\\cultivation' })
await invoke('generate_ca_files', { path: (await dataDir()) + '\\cultivation' })
}
async function generateInfo() {
console.log({
certificatePath: await dataDir() + '\\cultivation\\ca',
certificatePath: (await dataDir()) + '\\cultivation\\ca',
isAdmin: await invoke('is_elevated'),
connectingTo: proxyAddress
connectingTo: proxyAddress,
})
alert('check your dev console and send that in #cultivation')
}
@ -40,7 +40,7 @@ function none() {
alert('none')
}
class Debug extends React.Component{
class Debug extends React.Component {
render() {
return (
<div className="App">

View File

@ -4,10 +4,10 @@ import Close from '../../resources/icons/close.svg'
import './MiniDialog.css'
interface IProps {
children: React.ReactNode[] | React.ReactNode;
title?: string;
closeable?: boolean;
closeFn: () => void;
children: React.ReactNode[] | React.ReactNode
title?: string
closeable?: boolean
closeFn: () => void
}
export default class MiniDialog extends React.Component<IProps, never> {
@ -33,13 +33,12 @@ export default class MiniDialog extends React.Component<IProps, never> {
render() {
return (
<div className="MiniDialog" id="miniDialogContainer">
{
this.props.closeable !== undefined && this.props.closeable ?
{this.props.closeable !== undefined && this.props.closeable ? (
<div className="MiniDialogTop" id="miniDialogContainerTop" onClick={this.props.closeFn}>
<span>{this.props?.title}</span>
<img src={Close} className="MiniDialogClose" id="miniDialogButtonClose" />
</div> : null
}
</div>
) : null}
<div className="MiniDialogInner" id="miniDialogContent">
{this.props.children}

View File

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

View File

@ -11,26 +11,26 @@ import Server from '../../resources/icons/server.svg'
import Akebi from '../../resources/icons/akebi.svg'
import './ServerLaunchSection.css'
import {dataDir} from '@tauri-apps/api/path'
import { dataDir } from '@tauri-apps/api/path'
import { getGameExecutable } from '../../utils/game'
import { patchGame, unpatchGame } from '../../utils/metadata'
interface IState {
grasscutterEnabled: boolean;
buttonLabel: string;
checkboxLabel: string;
ip: string;
port: string;
grasscutterEnabled: boolean
buttonLabel: string
checkboxLabel: string
ip: string
port: string
ipPlaceholder: string;
portPlaceholder: string;
ipPlaceholder: string
portPlaceholder: string
portHelpText: string;
portHelpText: string
httpsLabel: string;
httpsEnabled: boolean;
httpsLabel: string
httpsEnabled: boolean
swag: boolean;
swag: boolean
}
export default class ServerLaunchSection extends React.Component<{}, IState> {
@ -48,7 +48,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
portHelpText: '',
httpsLabel: '',
httpsEnabled: false,
swag: false
swag: false,
}
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'),
httpsLabel: await translate('main.https_enable'),
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
this.setState({
grasscutterEnabled: config.toggle_grasscutter
grasscutterEnabled: config.toggle_grasscutter,
})
await saveConfig(config)
@ -94,7 +94,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
async playGame(exe?: string, proc_name?: string) {
const config = await getConfig()
if(!await getGameExecutable()) {
if (!(await getGameExecutable())) {
alert('Game executable not set!')
return
}
@ -117,14 +117,16 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
await setConfigOption('last_port', this.state.port)
await invoke('enable_process_watcher', {
process: proc_name || game_exe
process: proc_name || game_exe,
})
if (config.use_internal_proxy) {
// Set IP
await invoke('set_proxy_addr', { addr: (this.state.httpsEnabled ? 'https':'http') + '://' + this.state.ip + ':' + this.state.port })
await invoke('set_proxy_addr', {
addr: (this.state.httpsEnabled ? 'https' : 'http') + '://' + this.state.ip + ':' + this.state.port,
})
// Connect to proxy
await invoke('connect', { port: 8365, certificatePath: await dataDir() + '\\cultivation\\ca' })
await invoke('connect', { port: 8365, certificatePath: (await dataDir()) + '\\cultivation\\ca' })
}
// Open server as well if the options are set
@ -137,21 +139,23 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
await invoke('run_jar', {
path: config.grasscutter_path,
executeIn: jarFolder,
javaPath: config.java_path || ''
javaPath: config.java_path || '',
})
}
} else {
const unpatched = await unpatchGame()
if (!unpatched) {
alert(`Could not unpatch game, aborting launch! (You can find your metadata backup in ${await dataDir()}\\cultivation\\)`)
alert(
`Could not unpatch game, aborting launch! (You can find your metadata backup in ${await dataDir()}\\cultivation\\)`
)
return
}
}
// Launch the program
const gameExists = await invoke('dir_exists', {
path: exe || config.game_install_path
path: exe || config.game_install_path,
})
if (gameExists) await invoke('run_program', { path: exe || config.game_install_path })
@ -175,7 +179,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
await invoke('run_jar', {
path: config.grasscutter_path,
executeIn: jarFolder,
javaPath: config.java_path || ''
javaPath: config.java_path || '',
})
}
@ -194,7 +198,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
// First launch 3dm
invoke('run_program', {
path: config.migoto_path
path: config.migoto_path,
})
// Then play the game as normal
@ -203,13 +207,13 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
setIp(text: string) {
this.setState({
ip: text
ip: text,
})
}
setPort(text: string) {
this.setState({
port: text
port: text,
})
}
@ -220,7 +224,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
// Set state as well
this.setState({
httpsEnabled: config.https_enabled
httpsEnabled: config.https_enabled,
})
await saveConfig(config)
@ -230,29 +234,50 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
return (
<div id="playButton">
<div id="serverControls">
<Checkbox id="enableGC" label={this.state.checkboxLabel} onChange={this.toggleGrasscutter} checked={this.state.grasscutterEnabled}/>
<Checkbox
id="enableGC"
label={this.state.checkboxLabel}
onChange={this.toggleGrasscutter}
checked={this.state.grasscutterEnabled}
/>
</div>
{
this.state.grasscutterEnabled && (
{this.state.grasscutterEnabled && (
<div>
<div className="ServerConfig" id="serverConfigContainer">
<TextInput id="ip" key="ip" placeholder={this.state.ipPlaceholder} onChange={this.setIp} initalValue={this.state.ip} />
<TextInput style={{
<TextInput
id="ip"
key="ip"
placeholder={this.state.ipPlaceholder}
onChange={this.setIp}
initalValue={this.state.ip}
/>
<TextInput
style={{
width: '10%',
}} id="port" key="port" placeholder={this.state.portPlaceholder} onChange={this.setPort} initalValue={this.state.port} />
}}
id="port"
key="port"
placeholder={this.state.portPlaceholder}
onChange={this.setPort}
initalValue={this.state.port}
/>
<HelpButton contents={this.state.portHelpText} />
<Checkbox id="httpsEnable" label={this.state.httpsLabel} onChange={this.toggleHttps} checked={this.state.httpsEnabled} />
<Checkbox
id="httpsEnable"
label={this.state.httpsLabel}
onChange={this.toggleHttps}
checked={this.state.httpsEnabled}
/>
</div>
</div>
)
}
)}
<div className="ServerLaunchButtons" id="serverLaunchContainer">
<BigButton onClick={this.playGame} id="officialPlay">{this.state.buttonLabel}</BigButton>
{
this.state.swag && (
<BigButton onClick={this.playGame} id="officialPlay">
{this.state.buttonLabel}
</BigButton>
{this.state.swag && (
<>
<BigButton onClick={this.launchAkebi} id="akebiLaunch">
<img className="AkebiIcon" id="akebiIcon" src={Akebi} />
@ -261,9 +286,7 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
3DM
</BigButton>
</>
)
}
)}
<BigButton onClick={this.launchServer} id="serverLaunch">
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
</BigButton>

View File

@ -12,15 +12,15 @@ import './TopBar.css'
import { getConfig, setConfigOption } from '../../utils/configuration'
interface IProps {
optFunc: () => void;
downFunc: () => void;
gameFunc: () => void;
optFunc: () => void
downFunc: () => void
gameFunc: () => void
}
interface IState {
version: string;
clicks: number;
intv: NodeJS.Timeout | null;
version: string
clicks: number
intv: NodeJS.Timeout | null
}
export default class TopBar extends React.Component<IProps, IState> {
@ -30,7 +30,7 @@ export default class TopBar extends React.Component<IProps, IState> {
this.state = {
version: '0.0.0',
clicks: 0,
intv: null
intv: null,
}
this.activateClick = this.activateClick.bind(this)
@ -59,7 +59,7 @@ export default class TopBar extends React.Component<IProps, IState> {
setTimeout(() => {
// Gotta clear it so it goes back to regular colors
this.setState({
clicks: 0
clicks: 0,
})
}, 600)
@ -75,7 +75,7 @@ export default class TopBar extends React.Component<IProps, IState> {
if (this.state.clicks < 3) {
this.setState({
clicks: this.state.clicks + 1,
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500)
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500),
})
return
@ -89,29 +89,31 @@ export default class TopBar extends React.Component<IProps, IState> {
<span data-tauri-drag-region>
<Tr text="main.title" />
</span>
<span data-tauri-drag-region id="version">{this.state?.version}</span>
<span data-tauri-drag-region id="version">
{this.state?.version}
</span>
</div>
{
/**
{/**
* HEY YOU
*
* If you're looking at the source code to find the swag mode thing, that's okay! If you're not, move along...
* Just do me a favor and don't go telling everyone about how you found it. If you are just helping someone who
* for some reason needs it, that's fine, but not EVERYONE needs it, which is why it exists in the first place.
*/
}
<div id="unassumingButton" className={this.state.clicks === 2 ? 'spin' : ''} onClick={this.activateClick}>?</div>
*/}
<div id="unassumingButton" className={this.state.clicks === 2 ? 'spin' : ''} onClick={this.activateClick}>
?
</div>
<div className="TopBtns" id="topBarButtonContainer">
<div id="closeBtn" onClick={this.handleClose} className='TopButton'>
<div id="closeBtn" onClick={this.handleClose} className="TopButton">
<img src={closeIcon} alt="close" />
</div>
<div id="minBtn" onClick={this.handleMinimize} className='TopButton'>
<div id="minBtn" onClick={this.handleMinimize} className="TopButton">
<img src={minIcon} alt="minimize" />
</div>
<div id="settingsBtn" onClick={this.props.optFunc} className='TopButton'>
<div id="settingsBtn" onClick={this.props.optFunc} className="TopButton">
<img src={cogBtn} alt="settings" />
</div>
<div id="downloadsBtn" className='TopButton' onClick={this.props.downFunc}>
<div id="downloadsBtn" className="TopButton" onClick={this.props.downFunc}>
<img src={downBtn} alt="downloads" />
</div>
{/* <div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>

View File

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

View File

@ -1,4 +1,4 @@
.Checkbox input[type="checkbox"] {
.Checkbox input[type='checkbox'] {
display: none;
}
@ -17,7 +17,7 @@
.CheckboxDisplay img {
height: 100%;
filter: invert(99%) sepia(0%) saturate(1188%) hue-rotate(186deg) brightness(97%) contrast(67%)
filter: invert(99%) sepia(0%) saturate(1188%) hue-rotate(186deg) brightness(97%) contrast(67%);
}
.Checkbox label {

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import DownloadSection from './DownloadSection'
import './DownloadList.css'
interface IProps {
downloadManager: DownloadHandler;
downloadManager: DownloadHandler
}
export default class DownloadList extends React.Component<IProps, never> {
@ -16,17 +16,14 @@ export default class DownloadList extends React.Component<IProps, never> {
render() {
const list = this.props.downloadManager.getDownloads().map((download) => {
return (
<DownloadSection key={download.path} downloadName={download.path} downloadManager={this.props.downloadManager} />
<DownloadSection
key={download.path}
downloadName={download.path}
downloadManager={this.props.downloadManager}
/>
)
})
return (
<div className="DownloadList">
{
list.length > 0 ? list : 'No downloads present'
}
</div>
)
return <div className="DownloadList">{list.length > 0 ? list : 'No downloads present'}</div>
}
}

View File

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

View File

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

View File

@ -4,15 +4,15 @@ import Tr from '../../../utils/language'
import './ProgressBar.css'
interface IProps {
downloadManager: DownloadHandler,
downloadManager: DownloadHandler
}
interface IState {
average: number,
files: number,
extracting: number,
total: number,
speed: string,
average: number
files: number
extracting: number
total: number
speed: string
}
/**
@ -29,7 +29,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
files,
extracting,
total: totalSize,
speed: '0 B/s'
speed: '0 B/s',
}
}
@ -51,7 +51,9 @@ export default class ProgressBar extends React.Component<IProps, IState> {
return (
<div className="MainProgressBarWrapper">
<div className="ProgressBar">
<div className="InnerProgress" style={{
<div
className="InnerProgress"
style={{
width: `${(() => {
// Handles no files downloading
if (this.state.files === 0) {
@ -64,7 +66,8 @@ export default class ProgressBar extends React.Component<IProps, IState> {
return this.state.average
})()}%`,
}}></div>
}}
></div>
</div>
<div className="MainProgressText">

View File

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

View File

@ -7,14 +7,14 @@ import DownloadHandler from '../../../utils/download'
import { translate } from '../../../utils/language'
interface IProps {
path: string,
downloadManager: DownloadHandler,
path: string
downloadManager: DownloadHandler
}
interface IState {
progress: number,
status: string,
total: number,
progress: number
status: string
total: number
}
export default class ProgressBar extends React.Component<IProps, IState> {
@ -36,7 +36,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
const prog = this.props.downloadManager.getDownloadProgress(this.props.path)
this.setState({
progress: prog?.progress || 0,
status: await translate(`download_status.${prog?.status || 'stopped'}`) || 'stopped',
status: (await translate(`download_status.${prog?.status || 'stopped'}`)) || 'stopped',
total: prog?.total || 0,
})
@ -54,11 +54,15 @@ export default class ProgressBar extends React.Component<IProps, IState> {
render() {
return (
<div className="ProgressBarWrapper">
<div style={{
width: '80%'
}}>
<div
style={{
width: '80%',
}}
>
<div className="ProgressBar">
<div className="InnerProgress" style={{
<div
className="InnerProgress"
style={{
width: `${(() => {
// Handles files with content-lengths of 0
if (this.state.status === 'finished') {
@ -69,9 +73,10 @@ export default class ProgressBar extends React.Component<IProps, IState> {
return '0'
}
return this.state.progress / this.state.total * 100
return (this.state.progress / this.state.total) * 100
})()}%`,
}}></div>
}}
></div>
</div>
<div className="DownloadControls">
<div onClick={this.stopDownload} className="downloadStop">
@ -80,9 +85,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
</div>
</div>
<div className="ProgressText">
{capitalize(this.state.status) || 'Waiting'}
</div>
<div className="ProgressText">{capitalize(this.state.status) || 'Waiting'}</div>
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -16,8 +16,8 @@ import DownloadHandler from '../../../utils/download'
import * as meta from '../../../utils/metadata'
interface IProps {
closeFn: () => void;
downloadManager: DownloadHandler;
closeFn: () => void
downloadManager: DownloadHandler
}
interface IState {
@ -25,7 +25,7 @@ interface IState {
grasscutter_path: string
java_path: string
grasscutter_with_game: boolean
language_options: { [key: string]: string }[],
language_options: { [key: string]: string }[]
current_language: string
bg_url_or_path: string
themes: string[]
@ -61,7 +61,7 @@ export default class Options extends React.Component<IProps, IState> {
// Swag stuff
akebi_path: '',
migoto_path: ''
migoto_path: '',
}
this.setGameExecutable = this.setGameExecutable.bind(this)
@ -136,7 +136,7 @@ export default class Options extends React.Component<IProps, IState> {
setConfigOption('akebi_path', value)
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)
this.setState({
migoto_path: value
migoto_path: value,
})
}
@ -220,7 +220,7 @@ export default class Options extends React.Component<IProps, IState> {
async installCert() {
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 className="OptionValue" id="menuOptionsButtonmetaDownload">
<BigButton onClick={this.restoreMetadata} id="metaDownload">
<Tr text='components.download' />
<Tr text="components.download" />
</BigButton>
</div>
</div>
@ -270,11 +270,7 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="options.patch_metadata" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxPatchMeta">
<Checkbox
onChange={this.toggleMetadata}
checked={this.state?.patch_metadata}
id="patchMeta"
/>
<Checkbox onChange={this.toggleMetadata} checked={this.state?.patch_metadata} id="patchMeta" />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerUseProxy">
@ -282,18 +278,14 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="options.use_proxy" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxUseProxy">
<Checkbox
onChange={this.toggleProxy}
checked={this.state?.use_internal_proxy}
id="useProxy"
/>
<Checkbox onChange={this.toggleProxy} checked={this.state?.use_internal_proxy} id="useProxy" />
</div>
</div>
<Divider />
<div className='OptionSection' id="menuOptionsContainerGCJar">
<div className='OptionLabel' id="menuOptionsLabelGCJar">
<div className="OptionSection" id="menuOptionsContainerGCJar">
<div className="OptionLabel" id="menuOptionsLabelGCJar">
<Tr text="options.grasscutter_jar" />
</div>
<div className="OptionValue" id="menuOptionsDirGCJar">
@ -310,39 +302,34 @@ export default class Options extends React.Component<IProps, IState> {
</BigButton>
</div>
</div>
<div className='OptionSection' id="menuOptionsContainerInstallCert">
<div className='OptionLabel' id="menuOptionsLabelInstallCert">
<div className="OptionSection" id="menuOptionsContainerInstallCert">
<div className="OptionLabel" id="menuOptionsLabelInstallCert">
<Tr text="options.install_certificate" />
</div>
<div className='OptionValue' id="menuOptionsButtonInstallCert">
<div className="OptionValue" id="menuOptionsButtonInstallCert">
<BigButton disabled={false} onClick={this.installCert} id="installCert">
<Tr text="components.install" />
</BigButton>
</div>
</div>
{
this.state.swag && (
{this.state.swag && (
<>
<Divider />
<div className='OptionSection' id="menuOptionsContainerAkebi">
<div className='OptionLabel' id="menuOptionsLabelAkebi">
<div className="OptionSection" id="menuOptionsContainerAkebi">
<div className="OptionLabel" id="menuOptionsLabelAkebi">
<Tr text="swag.akebi" />
</div>
<div className='OptionValue' id="menuOptionsDirAkebi">
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
</div>
</div>
<div className='OptionSection' id="menuOptionsContainerMigoto">
<div className='OptionLabel' id="menuOptionsLabelMigoto">
<div className="OptionSection" id="menuOptionsContainerMigoto">
<div className="OptionLabel" id="menuOptionsLabelMigoto">
<Tr text="swag.migoto" />
</div>
<div className='OptionValue' id="menuOptionsDirMigoto">
<div className="OptionValue" id="menuOptionsDirMigoto">
<DirInput onChange={this.setMigoto} value={this.state?.migoto_path} extensions={['exe']} />
</div>
</div>
</div>
</>
)
}
)}
<Divider />

View File

@ -6,33 +6,33 @@ import Tr from '../../../utils/language'
import './NewsSection.css'
interface IProps {
selected?: string;
selected?: string
}
interface IState {
selected: string;
news?: JSX.Element;
commitList?: JSX.Element[];
selected: string
news?: JSX.Element
commitList?: JSX.Element[]
}
interface GrasscutterAPIResponse {
commits: {
gc_stable: CommitResponse[];
gc_dev: CommitResponse[];
cultivation: CommitResponse[];
gc_stable: CommitResponse[]
gc_dev: CommitResponse[]
cultivation: CommitResponse[]
}
}
interface CommitResponse {
sha: string;
commit: Commit;
sha: string
commit: Commit
}
interface Commit {
author: {
name: string;
};
message: string;
name: string
}
message: string
}
export default class NewsSection extends React.Component<IProps, IState> {
@ -65,14 +65,16 @@ export default class NewsSection extends React.Component<IProps, IState> {
try {
grasscutterApiResponse = JSON.parse(response)
} catch(e) {
} catch (e) {
grasscutterApiResponse = null
}
let commits: CommitResponse[]
if (grasscutterApiResponse?.commits == null) {
// If it didn't work, use official API
const response: string = await invoke('req_get', { url: 'https://api.github.com/repos/Grasscutters/Grasscutter/commits' })
const response: string = await invoke('req_get', {
url: 'https://api.github.com/repos/Grasscutters/Grasscutter/commits',
})
commits = JSON.parse(response)
} else {
commits = grasscutterApiResponse.commits.gc_stable
@ -86,15 +88,19 @@ export default class NewsSection extends React.Component<IProps, IState> {
const commitsListHtml = commitsList.map((commitResponse: CommitResponse) => {
return (
<tr className="Commit" id="newsCommitsTable" key={commitResponse.sha}>
<td className="CommitAuthor"><span>{commitResponse.commit.author.name}</span></td>
<td className="CommitMessage"><span>{commitResponse.commit.message}</span></td>
<td className="CommitAuthor">
<span>{commitResponse.commit.author.name}</span>
</td>
<td className="CommitMessage">
<span>{commitResponse.commit.message}</span>
</td>
</tr>
)
})
this.setState({
commitList: commitsListHtml,
news: <>{commitsListHtml}</>
news: <>{commitsListHtml}</>,
})
}
@ -104,7 +110,7 @@ export default class NewsSection extends React.Component<IProps, IState> {
async showNews() {
let news: JSX.Element | JSX.Element[] = <tr></tr>
switch(this.state.selected) {
switch (this.state.selected) {
case 'commits': {
const commits = await this.showLatestCommits()
if (commits != null) {
@ -114,16 +120,24 @@ export default class NewsSection extends React.Component<IProps, IState> {
}
case 'latest_version':
news = <tr><td>Latest version</td></tr>
news = (
<tr>
<td>Latest version</td>
</tr>
)
break
default:
news = <tr><td>Unknown</td></tr>
news = (
<tr>
<td>Unknown</td>
</tr>
)
break
}
this.setState({
news: <>{news}</>
news: <>{news}</>,
})
}
@ -131,17 +145,23 @@ export default class NewsSection extends React.Component<IProps, IState> {
return (
<div className="NewsSection" id="newsContainer">
<div className="NewsTabs" id="newsTabsContainer">
<div className={'NewsTab ' + (this.state.selected === 'commits' ? 'selected' : '')} id="commits" onClick={() => this.setSelected('commits')}>
<div
className={'NewsTab ' + (this.state.selected === 'commits' ? 'selected' : '')}
id="commits"
onClick={() => this.setSelected('commits')}
>
<Tr text="news.latest_commits" />
</div>
<div className={'NewsTab ' + (this.state.selected === 'latest_version' ? 'selected' : '')} id="latest_version" onClick={() => this.setSelected('latest_version')}>
<div
className={'NewsTab ' + (this.state.selected === 'latest_version' ? 'selected' : '')}
id="latest_version"
onClick={() => this.setSelected('latest_version')}
>
<Tr text="news.latest_version" />
</div>
</div>
<table className="NewsContent" id="newsContent">
<tbody>
{this.state.news}
</tbody>
<tbody>{this.state.news}</tbody>
</table>
</div>
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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