Merge pull request #8 from saturnclientmc/state-and-progress
State and progress
This commit is contained in:
+25
-24
@@ -1,35 +1,17 @@
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::{LauncherSettings, Mod, SharedState};
|
use crate::{save_state, LauncherSettings, Mod, SharedState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_installed_mods(state: State<SharedState>, version_id: String) -> Vec<Mod> {
|
pub fn get_installed_mods(state: State<SharedState>, version_id: String) -> Vec<Mod> {
|
||||||
let state = state.lock().unwrap();
|
Vec::new()
|
||||||
state
|
|
||||||
.mods
|
|
||||||
.iter()
|
|
||||||
.filter(|m| m.supported_versions.contains(&version_id))
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn toggle_mod(state: State<SharedState>, mod_id: String) {
|
pub fn toggle_mod(state: State<SharedState>, mod_id: String) {}
|
||||||
let mut state = state.lock().unwrap();
|
|
||||||
if let Some(mod_item) = state.mods.iter_mut().find(|m| m.id == mod_id) {
|
|
||||||
mod_item.enabled = !mod_item.enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn install_mod(state: State<SharedState>, mut mod_item: Mod) {
|
pub fn install_mod(state: State<SharedState>, mut mod_item: Mod) {}
|
||||||
let mut state = state.lock().unwrap();
|
|
||||||
|
|
||||||
if !state.mods.iter().any(|m| m.id == mod_item.id) {
|
|
||||||
mod_item.enabled = true;
|
|
||||||
state.mods.push(mod_item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_settings(state: State<SharedState>) -> LauncherSettings {
|
pub fn get_settings(state: State<SharedState>) -> LauncherSettings {
|
||||||
@@ -38,6 +20,25 @@ pub fn get_settings(state: State<SharedState>) -> LauncherSettings {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn update_settings(state: State<SharedState>, new_settings: LauncherSettings) {
|
pub fn update_settings(state: State<SharedState>, new_settings: LauncherSettings) {
|
||||||
let mut state = state.lock().unwrap();
|
{
|
||||||
state.settings = new_settings;
|
let mut state = state.lock().unwrap();
|
||||||
|
state.settings = new_settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
save_state(&state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_version(state: State<SharedState>) -> String {
|
||||||
|
state.lock().unwrap().version.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_version(state: State<SharedState>, version: String) {
|
||||||
|
{
|
||||||
|
let mut state = state.lock().unwrap();
|
||||||
|
state.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
save_state(&state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use lyceris::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use tauri::Emitter as _;
|
||||||
|
|
||||||
use crate::GameVersion;
|
use crate::GameVersion;
|
||||||
|
|
||||||
@@ -22,7 +23,16 @@ pub static LAUNCHER_DIR: Lazy<ProjectDirs> = Lazy::new(|| {
|
|||||||
});
|
});
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn launch_game(version: GameVersion, account: MinecraftAccount) -> Result<(), String> {
|
pub async fn launch_game(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
version: GameVersion,
|
||||||
|
account: MinecraftAccount,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
emit_progress(&app, 0.0);
|
||||||
|
|
||||||
|
app.emit("is-launching", "true").unwrap();
|
||||||
|
app.emit("launch-status", "Preparing").unwrap();
|
||||||
|
|
||||||
// Emitter uses `EventEmitter` inside of it
|
// Emitter uses `EventEmitter` inside of it
|
||||||
// and it uses tokio::Mutex for locking.
|
// and it uses tokio::Mutex for locking.
|
||||||
// That causes emitter methods to be async.
|
// That causes emitter methods to be async.
|
||||||
@@ -44,19 +54,18 @@ pub async fn launch_game(version: GameVersion, account: MinecraftAccount) -> Res
|
|||||||
// Java, libraries and assets are downloaded in parallel and
|
// Java, libraries and assets are downloaded in parallel and
|
||||||
// this event is triggered for each file.
|
// this event is triggered for each file.
|
||||||
emitter
|
emitter
|
||||||
.on(
|
.on(Event::MultipleDownloadProgress, {
|
||||||
Event::MultipleDownloadProgress,
|
let app = app.clone();
|
||||||
|(_, current, total, _): (String, u64, u64, String)| {
|
move |(_, current, total, _): (String, u64, u64, String)| {
|
||||||
|
emit_progress(&app, current as f32 / total as f32);
|
||||||
println!("Downloading {}/{}", current, total);
|
println!("Downloading {}/{}", current, total);
|
||||||
},
|
}
|
||||||
)
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Console event send when a line is printed to the console.
|
|
||||||
// It uses a seperated tokio thread to handle this operation.
|
|
||||||
emitter
|
emitter
|
||||||
.on(Event::Console, |line: String| {
|
.on(Event::Console, |line: String| {
|
||||||
println!("Line: {}", line);
|
println!("{}", line);
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -78,6 +87,8 @@ pub async fn launch_game(version: GameVersion, account: MinecraftAccount) -> Res
|
|||||||
|
|
||||||
println!("Starting installation");
|
println!("Starting installation");
|
||||||
|
|
||||||
|
app.emit("launch-status", "Installing").unwrap();
|
||||||
|
|
||||||
// Install method also checks for broken files
|
// Install method also checks for broken files
|
||||||
// and downloads them again if they are broken.
|
// and downloads them again if they are broken.
|
||||||
install::install(&config, Some(&emitter))
|
install::install(&config, Some(&emitter))
|
||||||
@@ -86,7 +97,10 @@ pub async fn launch_game(version: GameVersion, account: MinecraftAccount) -> Res
|
|||||||
|
|
||||||
println!("Finished installing");
|
println!("Finished installing");
|
||||||
|
|
||||||
// This method never downloads any file and just runs the game.
|
emit_progress(&app, 1.0);
|
||||||
|
|
||||||
|
app.emit("launch-status", "Launching").unwrap();
|
||||||
|
|
||||||
launch(&config, Some(&emitter))
|
launch(&config, Some(&emitter))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
@@ -94,5 +108,14 @@ pub async fn launch_game(version: GameVersion, account: MinecraftAccount) -> Res
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
app.emit("is-launching", "false").unwrap();
|
||||||
|
app.emit("launch-status", "").unwrap();
|
||||||
|
emit_progress(&app, 0.0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_progress(app: &tauri::AppHandle, progress: f32) {
|
||||||
|
let percent = (progress * 100.0).round() as u32;
|
||||||
|
let _ = app.emit("launch-progress", percent);
|
||||||
|
}
|
||||||
|
|||||||
+60
-25
@@ -2,7 +2,11 @@ pub mod commands;
|
|||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Mutex;
|
use std::{fs, path::PathBuf, sync::Mutex};
|
||||||
|
|
||||||
|
use crate::launcher::LAUNCHER_DIR;
|
||||||
|
|
||||||
|
type SharedState = Mutex<AppState>;
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@@ -31,43 +35,74 @@ pub struct LauncherSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- App State ---
|
// --- App State ---
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub mods: Vec<Mod>,
|
|
||||||
pub settings: LauncherSettings,
|
pub settings: LauncherSettings,
|
||||||
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
mods: vec![
|
|
||||||
Mod {
|
|
||||||
id: "sodium".into(),
|
|
||||||
name: "Sodium".into(),
|
|
||||||
version: "0.5.8".into(),
|
|
||||||
author: "jellysquid3".into(),
|
|
||||||
description: "Modern rendering engine for Minecraft.".into(),
|
|
||||||
enabled: true,
|
|
||||||
icon: None,
|
|
||||||
supported_versions: vec!["1.20.1".into(), "1.20.4".into(), "1.21".into()],
|
|
||||||
},
|
|
||||||
Mod {
|
|
||||||
id: "iris".into(),
|
|
||||||
name: "Iris".into(),
|
|
||||||
version: "1.7.0".into(),
|
|
||||||
author: "coderbot".into(),
|
|
||||||
description: "A modern shaders mod for Minecraft.".into(),
|
|
||||||
enabled: true,
|
|
||||||
icon: None,
|
|
||||||
supported_versions: vec!["1.20.1".into(), "1.20.4".into(), "1.21".into()],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
settings: LauncherSettings {
|
settings: LauncherSettings {
|
||||||
jvm_arguments: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200"
|
jvm_arguments: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200"
|
||||||
.into(),
|
.into(),
|
||||||
max_memory: 4096,
|
max_memory: 4096,
|
||||||
},
|
},
|
||||||
|
version: String::from("1.21.6"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SharedState = Mutex<AppState>;
|
// Helper to get config path
|
||||||
|
fn config_path() -> PathBuf {
|
||||||
|
LAUNCHER_DIR.config_dir().join("launcher.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Load ---
|
||||||
|
pub fn load_state() -> SharedState {
|
||||||
|
let path = config_path();
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
match fs::read_to_string(&path) {
|
||||||
|
Ok(content) => match serde_json::from_str::<AppState>(&content) {
|
||||||
|
Ok(state) => return Mutex::new(state),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to parse config, using default: {err}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to read config, using default: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If missing or failed → create default
|
||||||
|
let default = Mutex::new(AppState::default());
|
||||||
|
save_state(&default);
|
||||||
|
|
||||||
|
default
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Save ---
|
||||||
|
pub fn save_state(state: &SharedState) {
|
||||||
|
let path = config_path();
|
||||||
|
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
if let Err(err) = fs::create_dir_all(parent) {
|
||||||
|
eprintln!("Failed to create config directory: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::to_string_pretty(&*state.lock().unwrap()) {
|
||||||
|
Ok(json) => {
|
||||||
|
if let Err(err) = fs::write(path, json) {
|
||||||
|
eprintln!("Failed to write config: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to serialize config: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use saturn_launcher_lib::{commands::*, AppState};
|
use saturn_launcher_lib::load_state;
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.manage(Mutex::new(AppState::default()))
|
.manage(load_state())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// Saturn Launcher Commands
|
// Saturn Launcher Commands
|
||||||
get_installed_mods,
|
saturn_launcher_lib::commands::get_installed_mods,
|
||||||
toggle_mod,
|
saturn_launcher_lib::commands::toggle_mod,
|
||||||
install_mod,
|
saturn_launcher_lib::commands::install_mod,
|
||||||
get_settings,
|
saturn_launcher_lib::commands::get_settings,
|
||||||
update_settings,
|
saturn_launcher_lib::commands::update_settings,
|
||||||
|
saturn_launcher_lib::commands::get_version,
|
||||||
|
saturn_launcher_lib::commands::update_version,
|
||||||
// Launcher Commands
|
// Launcher Commands
|
||||||
saturn_launcher_lib::launcher::launch_game,
|
saturn_launcher_lib::launcher::launch_game,
|
||||||
// Auth Commands
|
// Auth Commands
|
||||||
|
|||||||
+9
-1
@@ -7,6 +7,7 @@ import SettingsSection from "./components/SettingsSection";
|
|||||||
import { versions } from "./lib/launcher";
|
import { versions } from "./lib/launcher";
|
||||||
import { GameVersion } from "./lib/types";
|
import { GameVersion } from "./lib/types";
|
||||||
import { MinecraftAccount } from "./lib/auth";
|
import { MinecraftAccount } from "./lib/auth";
|
||||||
|
import { getVersion, updateVersion } from "./lib/saturn";
|
||||||
|
|
||||||
type Section = "play" | "mods" | "settings";
|
type Section = "play" | "mods" | "settings";
|
||||||
|
|
||||||
@@ -50,9 +51,16 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedVersion(versions[0]);
|
(async () => {
|
||||||
|
const version = await getVersion();
|
||||||
|
setSelectedVersion(versions.find((v) => v.id == version) || null);
|
||||||
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedVersion?.id) updateVersion(selectedVersion?.id);
|
||||||
|
}, [selectedVersion]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Sidebar active={active} setActive={setActive} />
|
<Sidebar active={active} setActive={setActive} />
|
||||||
|
|||||||
@@ -16,22 +16,31 @@ interface PlaySectionProps {
|
|||||||
const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
||||||
const [isLaunching, setIsLaunching] = useState(false);
|
const [isLaunching, setIsLaunching] = useState(false);
|
||||||
const [launchStatus, setLaunchStatus] = useState("");
|
const [launchStatus, setLaunchStatus] = useState("");
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const unlistenProgress = listen<number>("launch-progress", (event) => {
|
||||||
|
setProgress(event.payload);
|
||||||
|
});
|
||||||
|
|
||||||
const unlistenLaunchStatus = listen<string>("launch-status", (event) => {
|
const unlistenLaunchStatus = listen<string>("launch-status", (event) => {
|
||||||
setLaunchStatus(event.payload);
|
setLaunchStatus(event.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlistenIsLaunching = listen<string>("is-launching", (event) => {
|
const unlistenIsLaunching = listen<string>("is-launching", (event) => {
|
||||||
setIsLaunching(event.payload === "true");
|
setIsLaunching(event.payload === "true");
|
||||||
|
if (event.payload === "false") setProgress(0); // reset when done
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
unlistenProgress.then((f) => f());
|
||||||
unlistenLaunchStatus.then((f) => f());
|
unlistenLaunchStatus.then((f) => f());
|
||||||
unlistenIsLaunching.then((f) => f());
|
unlistenIsLaunching.then((f) => f());
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
console.log(isLaunching);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
@@ -42,7 +51,7 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Centered Overlay */}
|
{/* Centered Overlay */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute -inset-1 bg-linear-to-r from-blue-600 to-cyan-500 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
<div className="absolute -inset-1 bg-linear-to-r from-blue-600 to-cyan-500 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||||
|
|
||||||
@@ -68,6 +77,25 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-64 transition-opacity duration-700 opacity-0 absolute mt-28",
|
||||||
|
isLaunching && "opacity-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="h-2 w-full bg-white/10 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-linear-to-r from-blue-500 to-cyan-400 transition-all duration-300"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-center mt-1 text-white/70">
|
||||||
|
{progress}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,3 +53,9 @@
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #3f3f46;
|
background: #3f3f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Define the fade-in animation */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,3 +65,12 @@ export async function updateSettings(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await invoke("update_settings", { newSettings });
|
await invoke("update_settings", { newSettings });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Version ---
|
||||||
|
export async function getVersion(): Promise<string> {
|
||||||
|
return await invoke("get_version");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateVersion(version: string): Promise<void> {
|
||||||
|
await invoke("update_version", { version });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user