320 lines
12 KiB
Rust
320 lines
12 KiB
Rust
use eframe::{egui, Frame};
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use crate::installer::validator::*;
|
|
use crate::installer::downloader::DownloadManager;
|
|
use crate::ui::download_progress::download_progress;
|
|
use crate::ui::game_selection::game_selection;
|
|
use crate::ui::installation_progress::installation_progress;
|
|
use crate::ui::path_selection::path_selection;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum GameType {
|
|
Diamond,
|
|
EnhancedEdition,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum InstallerState {
|
|
GameSelection,
|
|
PathSelection,
|
|
Download,
|
|
Installing,
|
|
Complete,
|
|
}
|
|
|
|
|
|
#[derive(Default)]
|
|
pub struct DownloadState {
|
|
pub progress: f32,
|
|
pub speed: f64,
|
|
pub estimated_remaining: std::time::Duration,
|
|
pub completed: bool,
|
|
pub files: Option<Vec<PathBuf>>,
|
|
pub error: Option<String>,
|
|
pub cancelled: bool,
|
|
}
|
|
|
|
pub struct SinfarInstallerApp<> {
|
|
pub state: InstallerState,
|
|
pub game_type: Option<GameType>,
|
|
pub install_path: Option<PathBuf>,
|
|
pub ee_exe_path: Option<PathBuf>,
|
|
pub download_progress: f32,
|
|
pub download_speed: f64,
|
|
pub estimated_remaining: std::time::Duration,
|
|
pub extraction_progress: f32,
|
|
pub download_error: Option<String>,
|
|
pub install_error: Option<String>,
|
|
pub download_state: std::sync::Arc<std::sync::Mutex<DownloadState>>,
|
|
pub eframe_ctx: Option<egui::Context>,
|
|
pub runtime: Arc<tokio::runtime::Runtime>
|
|
}
|
|
|
|
impl SinfarInstallerApp<> {
|
|
pub fn new(_cc: &eframe::CreationContext<>, runtime: Arc<tokio::runtime::Runtime>) -> Self {
|
|
Self {
|
|
state: InstallerState::GameSelection,
|
|
game_type: Some(GameType::Diamond),
|
|
install_path: None,
|
|
ee_exe_path: None,
|
|
download_progress: 0.0,
|
|
download_speed: 0.0,
|
|
estimated_remaining: std::time::Duration::new(0, 0),
|
|
extraction_progress: 0.0,
|
|
download_error: None,
|
|
install_error: None,
|
|
download_state: std::sync::Arc::new(std::sync::Mutex::new(DownloadState::default())),
|
|
eframe_ctx: None,
|
|
runtime, // Store the runtime
|
|
}
|
|
}
|
|
|
|
fn next_state(&mut self) {
|
|
self.state = match self.state {
|
|
InstallerState::GameSelection => InstallerState::PathSelection,
|
|
InstallerState::PathSelection => InstallerState::Download,
|
|
InstallerState::Download => InstallerState::Installing,
|
|
InstallerState::Installing => InstallerState::Complete,
|
|
InstallerState::Complete => return,
|
|
};
|
|
}
|
|
|
|
fn previous_state(&mut self) {
|
|
self.state = match self.state {
|
|
InstallerState::GameSelection => return,
|
|
InstallerState::PathSelection => InstallerState::GameSelection,
|
|
InstallerState::Download => InstallerState::PathSelection,
|
|
InstallerState::Installing => return, // Can't go back during installation
|
|
InstallerState::Complete => return,
|
|
};
|
|
}
|
|
}
|
|
|
|
impl<> eframe::App for SinfarInstallerApp<> {
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
|
|
self.eframe_ctx = Some(ctx.clone());
|
|
|
|
if self.state == InstallerState::Download {
|
|
if let Ok(state) = self.download_state.lock() {
|
|
// Set progress to 100% if completed
|
|
if state.completed {
|
|
self.download_progress = 1.0;
|
|
self.download_speed = 0.0;
|
|
self.estimated_remaining = std::time::Duration::from_secs(0);
|
|
} else if !state.cancelled {
|
|
self.download_progress = state.progress;
|
|
self.download_speed = state.speed;
|
|
self.estimated_remaining = state.estimated_remaining;
|
|
}
|
|
|
|
if let Some(error) = &state.error {
|
|
self.download_error = Some(error.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
ui.vertical_centered(|ui| {
|
|
ui.heading("Sinfar NWN Custom Content Installer");
|
|
ui.add_space(10.0);
|
|
|
|
match self.state {
|
|
InstallerState::GameSelection => {
|
|
game_selection::render(ui, self);
|
|
},
|
|
InstallerState::PathSelection => {
|
|
path_selection::render(ui, self);
|
|
},
|
|
InstallerState::Download => {
|
|
download_progress::render(ui, self);
|
|
},
|
|
InstallerState::Installing => {
|
|
installation_progress::render(ui, self);
|
|
},
|
|
InstallerState::Complete => {
|
|
ui.label("Installation complete!");
|
|
ui.label("Launch the game from the desktop shortcut to start playing.");
|
|
|
|
if ui.button("Close").clicked() {
|
|
std::process::exit(0);
|
|
}
|
|
},
|
|
}
|
|
});
|
|
|
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| {
|
|
ui.horizontal(|ui| {
|
|
|
|
let can_continue = match self.state {
|
|
InstallerState::GameSelection => self.game_type.is_some(),
|
|
InstallerState::PathSelection => {
|
|
match self.game_type {
|
|
Some(GameType::Diamond) => validator::validate_diamond_path(self.install_path.as_ref().unwrap()),
|
|
Some(GameType::EnhancedEdition) =>{
|
|
validator::validate_ee_path(self.install_path.as_ref().unwrap()) &&
|
|
validator::validate_ee_exe_path(self.ee_exe_path.as_ref().unwrap())
|
|
},
|
|
None => false,
|
|
}
|
|
},
|
|
InstallerState::Download => {
|
|
if let Ok(state) = self.download_state.lock() {
|
|
state.completed && state.files.is_some()
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
_ => true,
|
|
};
|
|
|
|
if self.state != InstallerState::Installing && self.state != InstallerState::Complete {
|
|
if ui.add_enabled(can_continue, egui::Button::new("Next")).clicked() {
|
|
self.next_state();
|
|
if self.state == InstallerState::Download {
|
|
self.start_download();
|
|
} else if self.state == InstallerState::Installing {
|
|
self.start_installation();
|
|
}
|
|
}
|
|
|
|
let can_go_back = match self.state {
|
|
InstallerState::Download => {
|
|
if let Ok(state) = self.download_state.lock() {
|
|
state.progress == 0.0 || state.completed || state.error.is_some()
|
|
} else {
|
|
true
|
|
}
|
|
},
|
|
InstallerState::GameSelection => false,
|
|
_ => true
|
|
};
|
|
|
|
if ui.add_enabled(can_go_back, egui::Button::new("Back")).clicked() {
|
|
self.previous_state();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Implementation of key installer functions
|
|
impl SinfarInstallerApp<> {
|
|
pub fn start_download(&mut self) {
|
|
// Reset progress and errors
|
|
self.download_progress = 0.0;
|
|
self.download_speed = 0.0;
|
|
self.estimated_remaining = std::time::Duration::from_secs(0);
|
|
self.download_error = None;
|
|
|
|
// Initialize download state
|
|
let download_state = std::sync::Arc::new(std::sync::Mutex::new(DownloadState::default()));
|
|
self.download_state = download_state.clone();
|
|
|
|
// Clone the game type for the async closure
|
|
let game_type = self.game_type.clone().unwrap();
|
|
|
|
// Create a context handle for requesting UI updates
|
|
let ctx = eframe::egui::Context::clone(self.eframe_ctx.as_ref().unwrap());
|
|
let repaint_ctx_progress = ctx.clone();
|
|
let repaint_ctx_complete = ctx.clone();
|
|
|
|
// Spawn the download task
|
|
self.runtime.spawn(async move {
|
|
// Create a download manager with progress callback
|
|
let progress_state = download_state.clone();
|
|
|
|
let manager = DownloadManager::new(move |progress, _file, speed, remaining| {
|
|
// Update progress through shared state
|
|
if let Ok(mut state) = progress_state.lock() {
|
|
state.progress = progress;
|
|
state.speed = speed;
|
|
state.estimated_remaining = remaining;
|
|
repaint_ctx_progress.request_repaint();
|
|
}
|
|
});
|
|
|
|
// Start the download process
|
|
match manager.download_all_files(&game_type).await {
|
|
Ok(files) => {
|
|
// Mark download as complete with the files
|
|
if let Ok(mut state) = download_state.lock() {
|
|
state.completed = true;
|
|
state.files = Some(files);
|
|
repaint_ctx_complete.request_repaint();
|
|
}
|
|
}
|
|
Err(e) => {
|
|
// Record the error
|
|
if let Ok(mut state) = download_state.lock() {
|
|
state.error = Some(format!("Download failed: {}", e));
|
|
ctx.request_repaint();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn cancel_download(&mut self) {
|
|
if let Ok(mut state) = self.download_state.lock() {
|
|
state.error = Some("Download cancelled".to_string());
|
|
state.progress = 0.0;
|
|
state.cancelled = true;
|
|
state.speed = 0.0;
|
|
state.estimated_remaining = std::time::Duration::from_secs(0);
|
|
}
|
|
self.download_progress = 0.0;
|
|
self.download_speed = 0.0;
|
|
self.estimated_remaining = std::time::Duration::from_secs(0);
|
|
}
|
|
|
|
pub fn start_installation(&mut self) {
|
|
// Use tokio runtime to handle the async installation
|
|
let install_path = self.install_path.clone().expect("Install path should be set");
|
|
let ee_exe_path = self.ee_exe_path.clone();
|
|
let download_state = self.download_state.clone();
|
|
let progress_state = self.download_state.clone();
|
|
let eframe_ctx = self.eframe_ctx.clone().expect("eframe context should be set");
|
|
let runtime = self.runtime.clone();
|
|
let game_type = self.game_type.clone().expect("Game type should be set");
|
|
|
|
// Create extraction manager with progress callback
|
|
let extractor = crate::installer::extractor::ExtractionManager::new(move |progress: f32, _filename: &str| {
|
|
if let Ok(mut app_state) = progress_state.lock() {
|
|
app_state.progress = progress;
|
|
}
|
|
eframe_ctx.request_repaint();
|
|
});
|
|
|
|
// Get downloaded files before spawning the async task
|
|
let downloaded_files = if let Ok(state) = download_state.lock() {
|
|
state.files.clone()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(files) = downloaded_files {
|
|
runtime.spawn(async move {
|
|
// Try to install the files
|
|
if let Err(e) = extractor.install_all_files(
|
|
&files,
|
|
&install_path,
|
|
&game_type,
|
|
ee_exe_path.as_deref()
|
|
).await {
|
|
if let Ok(mut app_state) = download_state.lock() {
|
|
app_state.error = Some(format!("Installation failed: {}", e));
|
|
}
|
|
} else {
|
|
// Mark as complete on success
|
|
if let Ok(mut app_state) = download_state.lock() {
|
|
app_state.completed = true;
|
|
app_state.progress = 1.0;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} |