Compare commits

...

22 Commits

Author SHA1 Message Date
SpikeHD
b0c545d032 correct jar for 2.8 run 2022-07-15 19:12:52 -07:00
SpikeHD
88dd7b854f default to latest game version 2022-07-15 19:07:22 -07:00
SpikeHD
e159bc2cdb resource getting for downloads page 2022-07-15 18:29:13 -07:00
SpikeHD
cc4600ec77 create resources file on ifrst config write 2022-07-15 18:25:43 -07:00
SpikeHD
f37e44a88c meta download button 2022-07-15 18:15:10 -07:00
SpikeHD
4f806efc93 client download 2022-07-15 18:00:05 -07:00
SpikeHD
083de896b3 get latest client and metadata links 2022-07-15 17:54:56 -07:00
SpikeHD
9c64c1f282 client version setting 2022-07-15 17:25:14 -07:00
SpikeHD
aa10a908ad idk some wip shit 2022-07-15 16:58:27 -07:00
SpikeHD
bae193050f window reload 2022-07-14 20:13:51 -07:00
SpikeHD
fc5ffae1e2 another normal feature 2022-07-14 20:05:11 -07:00
SpikeHD
ab0e05ffe1 scrolling with no scrollbars 2022-07-14 18:52:12 -07:00
SpikeHD
88a1740b91 normal feature 2022-07-14 18:49:49 -07:00
SpikeHD
f24f3af377 just another normal everyday feature, nothing to see here 2022-07-14 18:05:52 -07:00
SpikeHD
abafc94379 rework process watcher to be not terrible 2022-07-13 18:49:06 -07:00
SpikeHD
b2453e7c4d fix crash on denying UAC prompt 2022-07-13 18:26:06 -07:00
SpikeHD
4fc90ee333 cleanup and yarn update 2022-07-12 17:21:03 -07:00
SpikeHD
0ec8782f48 use thread properly lol 2022-07-12 17:15:32 -07:00
SpikeHD
33c67eef06 re-threadify program launching 2022-07-12 12:28:31 -07:00
SpikeHD
b3585927ca Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-07-11 19:45:29 -07:00
SpikeHD
3008f50e1f Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-07-08 22:21:41 -07:00
SpikeHD
99293ad7cf disable encryption toggle when no jar set 2022-07-06 17:57:17 -07:00
18 changed files with 6672 additions and 6366 deletions

View File

@@ -14,6 +14,8 @@
"enabled": "Enabled",
"disabled": "Disabled",
"game_exec": "Set Game Executable",
"game_version": "Set Game Version",
"emergency_metadata": "Emergency Metadata Restore",
"grasscutter_jar": "Set Grasscutter JAR",
"toggle_encryption": "Toggle Encryption",
"java_path": "Set Custom Java Path",
@@ -57,5 +59,8 @@
"gc_stable_data": "Download the current stable Grasscutter data files, which does not come with a jar file. This is useful for updating.",
"gc_dev_data": "Download the latest development Grasscutter data files, which does not come with a jar file. This is useful for updating.",
"resources": "These are also required to run a Grasscutter server. This button will be grey if you have an existing resources folder with contents inside"
},
"swag": {
"akebi": "Set Akebi Executable"
}
}

View File

@@ -5,7 +5,6 @@ windows_subsystem = "windows"
use once_cell::sync::Lazy;
use std::{sync::Mutex, collections::HashMap};
use std::path::PathBuf;
use std::thread;
use sysinfo::{System, SystemExt};
@@ -23,9 +22,6 @@ mod web;
static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
fn main() {
// Start the game process watcher.
process_watcher();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
enable_process_watcher,
@@ -53,22 +49,37 @@ fn main() {
downloader::stop_download,
lang::get_lang,
lang::get_languages,
web::valid_url
web::valid_url,
web::web_get
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn process_watcher() {
// Every 5 seconds, see if the game process is still running.
// If it is not, then we assume the game has closed and disable the proxy
// to prevent any requests from being sent to the game.
#[tauri::command]
fn is_game_running() -> bool {
// Grab the game process name
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
// Start a thread so as to not block the main thread.
thread::spawn(|| {
!proc.is_empty()
}
#[tauri::command]
fn enable_process_watcher(window: tauri::Window,process: String) {
*WATCH_GAME_PROCESS.lock().unwrap() = process;
window.listen("disable_process_watcher", |_e| {
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
});
println!("Starting process watcher...");
thread::spawn(move || {
let mut system = System::new_all();
loop {
thread::sleep(std::time::Duration::from_secs(5));
// Refresh system info
system.refresh_all();
@@ -81,28 +92,18 @@ fn process_watcher() {
// If the game process closes, disable the proxy.
if !exists {
println!("Game closed");
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
disconnect();
break;
}
}
thread::sleep(std::time::Duration::from_secs(5));
}
});
}
#[tauri::command]
fn is_game_running() -> bool {
// Grab the game process name
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
!proc.is_empty()
}
#[tauri::command]
fn enable_process_watcher(process: String) {
*WATCH_GAME_PROCESS.lock().unwrap() = process;
}
#[tauri::command]
async fn connect(port: u16, certificate_path: String) {
// Log message to console.

View File

@@ -1,11 +1,12 @@
use duct::cmd;
use crate::file_helpers;
#[tauri::command]
pub fn run_program(path: String) {
// Open the program from the specified path.
open::that(&path).unwrap();
// Open in new thread to prevent blocking.
std::thread::spawn(move || {
// Without unwrap_or, this can crash when UAC prompt is denied
open::that(&path).unwrap_or(());
});
}
#[tauri::command]

View File

@@ -15,4 +15,10 @@ pub(crate) async fn valid_url(url: String) -> bool {
let response = client.get(url).header(USER_AGENT, "cultivation").send().await.unwrap();
response.status().as_str() == "200"
}
#[tauri::command]
pub async fn web_get(url: String) -> String {
// Send a GET request to the specified URL and send the response body back to the client.
query(&url).await
}

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M7045 10204 c-301 -50 -596 -277 -731 -564 -96 -204 -111 -463 -40
-692 16 -52 14 -58 -26 -58 -62 0 -220 -85 -331 -178 -24 -20 -105 -91 -178
-157 -74 -66 -177 -156 -229 -200 -208 -175 -629 -573 -1001 -944 -152 -152
-286 -284 -297 -294 -18 -17 -27 -17 -75 -8 -96 21 -249 29 -361 20 -321 -26
-613 -162 -836 -389 -341 -346 -457 -846 -303 -1301 l36 -106 -66 -100 c-102
-155 -212 -343 -303 -518 -353 -683 -556 -1455 -531 -2025 6 -136 29 -341 47
-415 36 -156 58 -234 92 -335 47 -137 52 -147 134 -315 90 -182 113 -220 223
-375 376 -528 945 -931 1571 -1113 261 -76 439 -94 503 -52 27 17 30 17 105
-4 125 -35 209 -50 345 -62 107 -9 152 -8 279 6 118 13 180 15 277 8 214 -14
475 -9 566 11 144 31 312 80 395 114 14 6 68 27 120 48 91 36 302 135 365 172
180 104 373 265 507 422 346 407 518 977 520 1725 0 287 -10 448 -48 755 -43
357 -95 631 -208 1100 -24 102 -47 199 -51 217 -5 26 -1 41 25 78 48 70 108
204 132 297 32 118 31 366 0 482 -75 272 -258 503 -507 639 l-77 42 -68 202
c-37 112 -70 213 -73 225 -3 13 -14 57 -26 98 -12 41 -33 134 -47 205 -23 115
-26 154 -27 345 0 223 1 232 62 580 44 245 54 349 47 490 -10 234 -52 334
-206 495 -98 103 -128 150 -167 261 -24 70 -27 94 -28 209 0 135 10 185 58
280 49 99 182 235 282 288 179 96 373 112 464 39 62 -49 101 -117 101 -174 0
-81 -49 -152 -117 -172 -13 -3 -32 -8 -42 -11 -13 -4 -34 7 -65 34 -26 22 -63
45 -84 52 -107 32 -219 -73 -198 -185 7 -37 89 -141 129 -163 137 -76 279 -77
428 -3 102 52 190 149 236 263 36 89 42 239 14 326 -36 108 -112 215 -206 286
-80 62 -158 92 -265 105 -105 11 -142 11 -245 -6z m-574 -1640 c138 -41 189
-186 160 -459 -13 -115 -45 -328 -62 -405 -19 -86 -49 -386 -49 -493 0 -163
17 -299 71 -552 7 -36 35 -132 102 -350 l24 -80 -31 -2 c-114 -9 -233 -28
-305 -50 -193 -55 -383 -185 -509 -347 -12 -16 -24 -27 -27 -24 -6 5 -22 125
-30 223 -3 39 -10 113 -15 165 -10 105 -26 365 -35 595 -5 120 -11 158 -26
192 -34 70 -110 104 -187 84 -44 -12 -77 -47 -217 -230 -38 -51 -90 -118 -115
-150 -25 -32 -70 -91 -100 -131 -30 -40 -64 -84 -76 -98 l-21 -25 -37 67 c-89
159 -229 312 -386 419 l-81 55 213 214 c371 372 775 751 1063 997 94 79 193
166 220 192 78 75 185 157 239 184 60 30 136 33 217 9z m-2317 -1783 c351 -93
611 -365 682 -712 22 -109 15 -291 -16 -403 -107 -396 -472 -688 -900 -722
l-85 -6 70 50 c180 127 282 307 293 513 16 326 -203 611 -533 695 -90 22 -256
23 -347 1 -136 -33 -284 -119 -366 -212 -22 -25 -42 -45 -44 -45 -9 0 11 102
33 175 104 343 411 610 782 681 104 20 329 12 431 -15z m1310 -406 c21 -268
29 -351 51 -545 31 -262 72 -520 120 -765 26 -130 66 -320 75 -360 5 -22 28
-128 50 -235 23 -107 45 -213 50 -235 41 -178 113 -614 145 -880 48 -401 57
-943 20 -1215 -55 -407 -186 -801 -368 -1102 -107 -178 -168 -262 -258 -355
-146 -150 -290 -212 -494 -213 -252 0 -520 107 -800 319 -274 207 -539 609
-674 1021 -112 339 -154 603 -154 960 0 278 18 425 82 671 92 348 301 725 615
1106 l50 61 87 16 c472 86 871 430 1017 876 42 128 55 211 58 383 l4 168 120
157 c169 220 194 252 196 249 1 -1 5 -38 8 -82z m1043 -511 c-144 -112 -216
-335 -168 -519 27 -101 69 -171 148 -245 74 -69 141 -104 246 -127 67 -14 90
-15 159 -5 145 21 277 102 353 218 60 90 86 178 86 287 0 51 -3 102 -6 112
-13 40 26 -17 61 -89 95 -196 84 -447 -28 -633 -183 -303 -569 -423 -917 -284
-171 69 -330 226 -398 394 -152 373 46 791 432 913 73 23 82 17 32 -22z
m-3393 -1044 c104 -70 239 -133 356 -166 52 -15 97 -28 98 -30 4 -2 -15 -29
-83 -124 -60 -83 -171 -254 -217 -335 -185 -323 -305 -691 -349 -1070 -19
-158 -16 -519 5 -690 34 -284 98 -554 186 -790 44 -120 179 -400 231 -480 171
-266 299 -420 509 -610 50 -45 61 -59 47 -63 -47 -10 -352 117 -535 224 -700
409 -1152 1060 -1253 1804 -19 136 -16 466 5 625 78 591 314 1222 678 1813
l70 113 87 -84 c48 -46 122 -107 165 -137z m3105 -469 c175 -88 288 -116 481
-116 190 0 316 30 470 111 30 16 58 30 61 32 15 7 131 -523 183 -833 64 -382
88 -661 88 -1015 -1 -346 -24 -553 -94 -820 -112 -430 -342 -768 -675 -993
-139 -94 -386 -208 -643 -297 -67 -24 -174 -50 -255 -64 -94 -17 -350 -22
-359 -8 -2 4 16 26 40 47 42 37 171 184 204 231 176 256 269 425 360 654 118
295 181 554 221 907 19 168 21 222 16 503 -4 173 -11 369 -16 434 -30 341
-103 828 -177 1168 -15 73 -18 108 -7 108 4 0 50 -22 102 -49z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -62,20 +62,27 @@
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-items: flex-start;
height: 100%;
max-height: 60px;
}
#officialPlay {
width: 60%
.ServerLaunchButtons .BigButton {
margin-right: 8px;
}
#officialPlay {
max-width: 60%;
flex-grow: 1;
}
#akebiLaunch,
#serverLaunch {
width: 5%;
}
.AkebiIcon,
.ServerIcon {
height: 20px;
filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%);

View File

@@ -8,6 +8,8 @@ import { translate } from '../../utils/language'
import { invoke } from '@tauri-apps/api/tauri'
import Server from '../../resources/icons/server.svg'
import Akebi from '../../resources/icons/akebi.svg'
import './ServerLaunchSection.css'
import {dataDir} from '@tauri-apps/api/path'
@@ -29,6 +31,8 @@ interface IState {
httpsLabel: string;
httpsEnabled: boolean;
swag: boolean;
}
export default class ServerLaunchSection extends React.Component<IProps, IState> {
@@ -45,11 +49,13 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
portPlaceholder: '',
portHelpText: '',
httpsLabel: '',
httpsEnabled: false
httpsEnabled: false,
swag: false
}
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
this.playGame = this.playGame.bind(this)
this.launchAkebi = this.launchAkebi.bind(this)
this.setIp = this.setIp.bind(this)
this.setPort = this.setPort.bind(this)
this.toggleHttps = this.toggleHttps.bind(this)
@@ -69,6 +75,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
portHelpText: await translate('help.port_help_text'),
httpsLabel: await translate('main.https_enable'),
httpsEnabled: config.https_enabled || false,
swag: config.swag_mode || false
})
}
@@ -85,7 +92,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
await saveConfig(config)
}
async playGame() {
async playGame(exe?: string, proc_name?: string) {
const config = await getConfig()
if (!config.game_install_path) return alert('Game path not set!')
@@ -107,7 +114,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
// Set IP
await invoke('set_proxy_addr', { addr: (this.state.httpsEnabled ? 'https':'http') + '://' + this.state.ip + ':' + this.state.port })
await invoke('enable_process_watcher', {
process: game_exe
process: proc_name || game_exe
})
// Connect to proxy
@@ -133,11 +140,11 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
// Launch the program
const gameExists = await invoke('dir_exists', {
path: config.game_install_path
path: exe || config.game_install_path
})
if (gameExists) await invoke('run_program', { path: config.game_install_path })
else alert('Game not found! At: ' + config.game_install_path)
if (gameExists) await invoke('run_program', { path: exe || config.game_install_path })
else alert('Game not found! At: ' + (exe || config.game_install_path))
}
async launchServer() {
@@ -161,6 +168,16 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
})
}
async launchAkebi() {
const config = await getConfig()
// Get game exe from game path, so we can watch it
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
const gameExec = pathArr[pathArr.length - 1]
await this.playGame(config.akebi_path, gameExec)
}
setIp(text: string) {
this.setState({
ip: text
@@ -205,13 +222,19 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
<Checkbox id="httpsEnable" label={this.state.httpsLabel} onChange={this.toggleHttps} checked={this.state.httpsEnabled} />
</div>
</div>
)
}
<div className="ServerLaunchButtons" id="serverLaunchContainer">
<BigButton onClick={this.playGame} id="officialPlay">{this.state.buttonLabel}</BigButton>
{
this.state.swag && (
<BigButton onClick={this.launchAkebi} id="akebiLaunch">
<img className="AkebiIcon" id="akebiIcon" src={Akebi} />
</BigButton>
)
}
<BigButton onClick={this.launchServer} id="serverLaunch">
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
</BigButton>

View File

@@ -25,4 +25,30 @@
#version {
margin: 0px 6px;
color: #434343;
}
#unassumingButton {
font-weight: bold;
margin: 0px 6px;
color: #141414;
transition: color 0.2s ease-in-out;
}
#unassumingButton:hover {
color: #434343;
}
#unassumingButton.spin {
color: #fff;
animation: spin 0.5s ease-in-out;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -10,6 +10,7 @@ import gameBtn from '../../resources/icons/game.svg'
import Tr from '../../utils/language'
import './TopBar.css'
import { getConfig, setConfigOption } from '../../utils/configuration'
interface IProps {
optFunc: () => void;
@@ -19,13 +20,21 @@ interface IProps {
interface IState {
version: string;
clicks: number;
intv: NodeJS.Timeout | null;
}
export default class TopBar extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = { version: '0.0.0' }
this.state = {
version: '0.0.0',
clicks: 0,
intv: null
}
this.activateClick = this.activateClick.bind(this)
}
async componentDidMount() {
@@ -41,6 +50,39 @@ export default class TopBar extends React.Component<IProps, IState> {
appWindow.minimize()
}
async activateClick() {
const config = await getConfig()
// They already got it, no need to reactivate
if (config.swag_mode) return
if (this.state.clicks === 2) {
setTimeout(() => {
// Gotta clear it so it goes back to regular colors
this.setState({
clicks: 0
})
}, 600)
// Activate... SWAG MODE
await setConfigOption('swag_mode', true)
// Reload the window
window.location.reload()
return
}
if (this.state.clicks < 3) {
this.setState({
clicks: this.state.clicks + 1,
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500)
})
return
}
}
render() {
return (
<div className="TopBar" id="topBarContainer" data-tauri-drag-region>
@@ -50,6 +92,16 @@ export default class TopBar extends React.Component<IProps, IState> {
</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 className="TopBtns" id="topBarButtonContainer">
<div id="closeBtn" onClick={this.handleClose} className='TopButton'>
<img src={closeIcon} alt="close" />
@@ -63,9 +115,9 @@ export default class TopBar extends React.Component<IProps, IState> {
<div id="downloadsBtn" className='TopButton' onClick={this.props.downFunc}>
<img src={downBtn} alt="downloads" />
</div>
{/* <div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>
<div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>
<img src={gameBtn} alt="game" />
</div> */}
</div>
</div>
</div>
)

View File

@@ -12,12 +12,7 @@ import { getConfigOption, setConfigOption } from '../../../utils/configuration'
import { invoke } from '@tauri-apps/api'
import { listen } from '@tauri-apps/api/event'
import HelpButton from '../common/HelpButton'
const STABLE_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/stable.zip'
const DEV_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip'
const STABLE_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip'
const DEV_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip'
const RESOURCES_DOWNLOAD = 'https://github.com/Koko-boya/Grasscutter_Resources/archive/refs/heads/main.zip'
import { getVersionCache, VersionData } from '../../../utils/resources'
interface IProps {
closeFn: () => void;
@@ -30,6 +25,7 @@ interface IState {
repo_downloading: boolean
grasscutter_set: boolean
resources_exist: boolean
version_data: VersionData | null
}
export default class Downloads extends React.Component<IProps, IState> {
@@ -41,7 +37,8 @@ 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,
version_data: null
}
this.getGrasscutterFolder = this.getGrasscutterFolder.bind(this)
@@ -55,6 +52,11 @@ export default class Downloads extends React.Component<IProps, IState> {
async componentDidMount() {
const gc_path = await getConfigOption('grasscutter_path')
const versionData = await getVersionCache()
this.setState({
version_data: versionData,
})
listen('jar_extracted', () => {
this.setState({ grasscutter_set: true }, this.forceUpdate)
@@ -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(this.state.version_data?.stable, 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(this.state.version_data?.dev, 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(this.state.version_data?.stableJar, 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(this.state.version_data?.devJar, folder + '\\grasscutter.zip', () =>{
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
})
@@ -151,7 +153,7 @@ export default class Downloads extends React.Component<IProps, IState> {
async downloadResources() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(RESOURCES_DOWNLOAD, folder + '\\resources.zip', async () => {
this.props.downloadManager.addDownload(this.state.version_data?.resources, folder + '\\resources.zip', async () => {
// Delete the existing folder if it exists
if (await invoke('dir_exists', {
path: folder + '\\resources'
@@ -200,7 +202,7 @@ export default class Downloads extends React.Component<IProps, IState> {
</HelpButton>
</div>
<div className='DownloadValue' id="downloadMenuButtonGCStable">
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterStable} id="grasscutterStableBtn" >
<BigButton disabled={this.state.grasscutter_downloading || !this.state.version_data?.stableJar} onClick={this.downloadGrasscutterStable} id="grasscutterStableBtn" >
<Tr text="components.download" />
</BigButton>
</div>
@@ -215,7 +217,7 @@ export default class Downloads extends React.Component<IProps, IState> {
</HelpButton>
</div>
<div className='DownloadValue' id="downloadMenuButtonGCDev">
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterLatest} id="grasscutterLatestBtn" >
<BigButton disabled={this.state.grasscutter_downloading || !this.state.version_data?.devJar} onClick={this.downloadGrasscutterLatest} id="grasscutterLatestBtn" >
<Tr text="components.download" />
</BigButton>
</div>
@@ -233,7 +235,7 @@ export default class Downloads extends React.Component<IProps, IState> {
</HelpButton>
</div>
<div className='DownloadValue' id="downloadMenuButtonGCStableData">
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterStableRepo" >
<BigButton disabled={this.state.repo_downloading || !this.state.version_data?.stable} onClick={this.downloadGrasscutterStableRepo} id="grasscutterStableRepo" >
<Tr text="components.download" />
</BigButton>
</div>
@@ -248,7 +250,7 @@ export default class Downloads extends React.Component<IProps, IState> {
</HelpButton>
</div>
<div className='DownloadValue' id="downloadMenuButtonGCDevData">
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterDevRepo" >
<BigButton disabled={this.state.repo_downloading || !this.state.version_data?.dev} onClick={this.downloadGrasscutterStableRepo} id="grasscutterDevRepo" >
<Tr text="components.download" />
</BigButton>
</div>
@@ -264,7 +266,11 @@ export default class Downloads extends React.Component<IProps, IState> {
</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" >
<BigButton disabled={
this.state.resources_downloading
|| !this.state.grasscutter_set
|| this.state.resources_exist
|| !this.state.version_data?.resources} onClick={this.downloadResources} id="resourcesBtn" >
<Tr text="components.download" />
</BigButton>
</div>

View File

@@ -8,8 +8,7 @@ import DirInput from '../common/DirInput'
import BigButton from '../common/BigButton'
import HelpButton from '../common/HelpButton'
import { unzip } from '../../../utils/zipUtils'
const GAME_DOWNLOAD = ''
import { getVersionCache } from '../../../utils/resources'
interface IProps {
closeFn: () => void;
@@ -20,6 +19,7 @@ interface IState {
gameDownloading: boolean;
gameDownloadFolder: string;
dirPlaceholder: string;
clientDownloadLink: string | null | undefined;
}
export default class Downloads extends React.Component<IProps, IState> {
@@ -29,23 +29,25 @@ export default class Downloads extends React.Component<IProps, IState> {
this.state = {
gameDownloading: false,
gameDownloadFolder: '',
dirPlaceholder: ''
dirPlaceholder: '',
clientDownloadLink: ''
}
this.downloadGame = this.downloadGame.bind(this)
}
async componentDidMount() {
this.setState({
dirPlaceholder: await translate('components.select_folder')
})
const versionCache = await getVersionCache()
console.log(this.state)
this.setState({
dirPlaceholder: await translate('components.select_folder'),
clientDownloadLink: versionCache?.client_download_link
})
}
async downloadGame() {
const folder = this.state.gameDownloadFolder
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', () =>{
this.props.downloadManager.addDownload(this.state.clientDownloadLink, folder + '\\game.zip', () =>{
unzip(folder + '\\game.zip', folder + '\\', () => {
this.setState({
gameDownloading: false
@@ -63,7 +65,7 @@ export default class Downloads extends React.Component<IProps, IState> {
<Menu heading='Download Game' closeFn={this.props.closeFn} className="GameDownloadMenu">
<div className="GameDownload">
{
this.state.gameDownloadFolder !== '' && !this.state.gameDownloading ?
this.state.gameDownloadFolder !== '' && !this.state.gameDownloading && this.state.clientDownloadLink ?
<BigButton id="downloadGameBtn" onClick={this.downloadGame}>Download Game</BigButton>
: <BigButton id="disabledGameBtn" onClick={() => null} disabled>Download Game</BigButton>
}

View File

@@ -14,6 +14,12 @@
border-radius: 10px;
box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.2);
overflow-y: auto;
}
.Menu::-webkit-scrollbar {
display: none;
}
.MenuInner {

View File

@@ -12,6 +12,7 @@ import * as server from '../../../utils/server'
import './Options.css'
import BigButton from '../common/BigButton'
import { cacheLauncherResources, getVersionCache, getVersions } from '../../../utils/resources'
interface IProps {
closeFn: () => void;
@@ -20,6 +21,8 @@ interface IProps {
interface IState {
game_install_path: string
grasscutter_path: string
client_version: string
meta_download: string | null | undefined
java_path: string
grasscutter_with_game: boolean
language_options: { [key: string]: string }[],
@@ -28,6 +31,10 @@ interface IState {
themes: string[]
theme: string
encryption: boolean
swag: boolean
// Swag stuff
akebi_path: string
}
export default class Options extends React.Component<IProps, IState> {
@@ -37,6 +44,8 @@ export default class Options extends React.Component<IProps, IState> {
this.state = {
game_install_path: '',
grasscutter_path: '',
client_version: '',
meta_download: '',
java_path: '',
grasscutter_with_game: false,
language_options: [],
@@ -44,12 +53,18 @@ export default class Options extends React.Component<IProps, IState> {
bg_url_or_path: '',
themes: ['default'],
theme: '',
encryption: false
encryption: false,
swag: false,
// Swag stuff
akebi_path: '',
}
this.setGameExec = this.setGameExec.bind(this)
this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
this.setJavaPath = this.setJavaPath.bind(this)
this.setClientVersion = this.setClientVersion.bind(this)
this.setAkebi = this.setAkebi.bind(this)
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
this.setCustomBackground = this.setCustomBackground.bind(this)
this.toggleEncryption = this.toggleEncryption.bind(this)
@@ -68,13 +83,19 @@ export default class Options extends React.Component<IProps, IState> {
game_install_path: config.game_install_path || '',
grasscutter_path: config.grasscutter_path || '',
java_path: config.java_path || '',
client_version: config.client_version || '',
meta_download: (await getVersionCache())?.metadata_backup_link || '',
grasscutter_with_game: config.grasscutter_with_game || false,
language_options: languages,
current_language: config.language || 'en',
bg_url_or_path: config.customBackground || '',
themes: (await getThemeList()).map(t => t.name),
theme: config.theme || 'default',
encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled')
encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled'),
swag: config.swag_mode || false,
// Swag stuff
akebi_path: config.akebi_path || '',
})
this.forceUpdate()
@@ -88,6 +109,17 @@ export default class Options extends React.Component<IProps, IState> {
})
}
async setClientVersion(value: string) {
await setConfigOption('client_version', value)
const newCache = await cacheLauncherResources()
this.setState({
client_version: value,
meta_download: newCache?.metadata_backup_link
})
}
setGrasscutterJar(value: string) {
setConfigOption('grasscutter_path', value)
@@ -104,6 +136,14 @@ export default class Options extends React.Component<IProps, IState> {
})
}
setAkebi(value: string) {
setConfigOption('akebi_path', value)
this.setState({
akebi_path: value
})
}
async setLanguage(value: string) {
await setConfigOption('language', value)
window.location.reload()
@@ -178,6 +218,51 @@ export default class Options extends React.Component<IProps, IState> {
<DirInput onChange={this.setGameExec} value={this.state?.game_install_path} extensions={['exe']} />
</div>
</div>
<div className='OptionSection' id="menuOptionsContainerClientVersion">
<div className='OptionLabel' id="menuOptionsLabelClientVersion">
<Tr text="options.game_version" />
</div>
<div className='OptionValue' id="menuOptionsDirClientVersion">
<select value={this.state.client_version} id="menuOptionsSelectMenuThemes" onChange={(event) => {
this.setClientVersion(event.target.value)
}}>
{getVersions().map(t => (
<option
key={t}
value={t}>
{t}
</option>
))}
</select>
</div>
</div>
<div className='OptionSection' id="menuOptionsContainerMetadataDownload">
<div className='OptionLabel' id="menuOptionsLabelMetadataDownload">
<Tr text="options.emergency_metadata" />
</div>
<div className='OptionValue' id="menuOptionsButtonMetadataDownload">
<BigButton disabled={this.state.meta_download === ''} onClick={this.toggleEncryption} id="toggleEnc">
<Tr text='components.download' />
</BigButton>
</div>
</div>
{
this.state.swag && (
<>
<Divider />
<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>
<Divider />
</>
)
}
<div className='OptionSection' id="menuOptionsContainerGCJar">
<div className='OptionLabel' id="menuOptionsLabelGCJar">
<Tr text="options.grasscutter_jar" />
@@ -191,7 +276,7 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="options.toggle_encryption" />
</div>
<div className='OptionValue' id="menuOptionsButtonToggleEnc">
<BigButton onClick={this.toggleEncryption} id="toggleEnc">
<BigButton disabled={this.state.grasscutter_path === ''} onClick={this.toggleEncryption} id="toggleEnc">
{
this.state.encryption
}

View File

@@ -1,5 +1,6 @@
import { fs } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path'
import { cacheLauncherResources } from './resources'
let configFilePath: string
let defaultConfig: Configuration
@@ -17,6 +18,7 @@ let defaultConfig: Configuration
last_port: '443',
language: 'en',
customBackground: '',
client_version: '2.7.0',
cert_generated: false,
theme: 'default',
https_enabled: false,
@@ -39,10 +41,15 @@ export interface Configuration {
last_port: string
language: string
customBackground: string
client_version: string
cert_generated: boolean
theme: string
https_enabled: boolean
debug_enabled: boolean
swag_mode?: boolean
// Swag stuff
akebi_path?: string
}
export async function setConfigOption(key: string, value: any): Promise<void> {
@@ -112,6 +119,12 @@ async function readConfigFile() {
contents: JSON.stringify(defaultConfig)
}
// Also just shoe-horning this in, cache resources on first launch
const versionData = await cacheLauncherResources()
defaultConfig.client_version = versionData?.game || ''
// Write config
await fs.writeFile(file)
}

View File

@@ -107,7 +107,9 @@ export default class DownloadHandler {
return this.downloads.some(d => d.path.includes('grasscutter_repo.zip'))
}
addDownload(url: string, path: string, onFinish?: () => void) {
addDownload(url: string | null | undefined, path: string, onFinish?: () => void) {
if (!url) return
// Begin download from rust backend, don't add if the download addition fails
invoke('download_file', { url, path })
const obj = {

106
src/utils/resources.ts Normal file
View File

@@ -0,0 +1,106 @@
import { fs, invoke } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path'
import { getConfig } from './configuration'
export interface VersionData {
game: string
metadata: string | null
metadata_backup_link: string | null
client_download_link: string | null
resources: string
stableJar: string | null
devJar: string | null
stable: string | null
dev: string | null
}
const globals: {
[key: string]: VersionData
} = {
'2.8.0': {
game: '2.8.0',
metadata: '2.8.0',
metadata_backup_link: null,
client_download_link: null,
resources: 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/2.8/GrasscutterResources-2.8.zip',
stableJar: null,
devJar: 'https://nightly.link/Grasscutters/Grasscutter/actions/runs/2661955213/Grasscutter.zip',
stable: null,
dev: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/2.8.zip'
},
'2.7.0': {
game: '2.7.0',
metadata: null,
metadata_backup_link: null,
client_download_link: null,
resources: 'https://github.com/Koko-boya/Grasscutter_Resources/archive/refs/heads/main.zip',
stableJar: 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip',
devJar: 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip',
stable: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/stable.zip',
dev: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip'
},
'2.6.0': {
game: '2.6.0',
metadata: null,
metadata_backup_link: null,
client_download_link: null,
resources: 'https://github.com/Koko-boya/Grasscutter_Resources/archive/0e99a59218a346c2d56c54953f99077882de4a6d.zip',
stableJar: 'https://github.com/Grasscutters/Grasscutter/releases/download/v1.1.0/grasscutter-1.1.0.jar',
devJar: null,
stable: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/2.6.zip',
dev: null
}
}
export async function cacheLauncherResources() {
const config = await getConfig()
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
}))
if (!versions || versions.retcode !== 0) {
console.log('Failed to get versions from API')
return null
}
const selectedVersion = config.client_version || versions.data.game.latest.version
const selectedVersionData = globals[selectedVersion]
if (!selectedVersionData) {
console.log('Failed to get version for selected version')
return null
}
const latest = versions.data.game.latest
const latestData = globals[latest.version]
if (latestData) {
latestData.metadata_backup_link = latest.decompressed_path + '/GenshinImpact_Data/Managed/Metadata/global-metadata.dat'
latestData.client_download_link = latest.path
}
// Write
fs.writeFile({
path: await dataDir() + 'cultivation/resources.json',
contents: JSON.stringify(selectedVersionData)
})
// In case we want to get it right away too
return selectedVersionData
}
export async function getVersionCache() {
const raw = await fs.readTextFile(await dataDir() + 'cultivation/resources.json').catch(e => {
console.log(e)
return null
})
return raw ? JSON.parse(raw) as VersionData : null
}
export function getVersions() {
return Object.keys(globals)
}

View File

@@ -18,7 +18,7 @@ export async function toggleEncryption(path: string) {
// Write file
await fs.writeFile({
path,
contents: JSON.stringify(serverConf)
contents: JSON.stringify(serverConf, null, 2),
})
}

12484
yarn.lock

File diff suppressed because it is too large Load Diff