Rust WIP
This commit is contained in:
		
							parent
							
								
									ea9b68b235
								
							
						
					
					
						commit
						6f90e4bf68
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,3 +1,4 @@
 | 
			
		||||
SinfarInstaller-v30.exe
 | 
			
		||||
win32_8181.7z
 | 
			
		||||
sinfar_all_files_v30.7z
 | 
			
		||||
rust/target
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4795
									
								
								rust/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4795
									
								
								rust/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										31
									
								
								rust/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								rust/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "sinfar-installer"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
authors = ["Your Name"]
 | 
			
		||||
description = "Sinfar installer application"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
eframe = "0.23.0"
 | 
			
		||||
rfd = "0.12.0"              # For file dialogs
 | 
			
		||||
reqwest = { version = "0.11", features = ["blocking", "json", "stream"] }
 | 
			
		||||
tokio = { version = "1", features = ["full"] }
 | 
			
		||||
futures-util = "0.3"
 | 
			
		||||
sevenz-rust = "0.5.0"       # For 7z extraction
 | 
			
		||||
dirs = "5.0"                # For finding common directories
 | 
			
		||||
anyhow = "1.0"              # Error handling
 | 
			
		||||
log = "0.4"
 | 
			
		||||
env_logger = "0.10"
 | 
			
		||||
serde = { version = "1.0", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0"
 | 
			
		||||
once_cell = "1.18"          # For lazy statics
 | 
			
		||||
image = "0.24"              # For icon handling
 | 
			
		||||
zip = "2.6.1"
 | 
			
		||||
 | 
			
		||||
# Add this to handle Windows-specific dependencies
 | 
			
		||||
[target.'cfg(windows)'.dependencies]
 | 
			
		||||
winreg = "0.51"            # For Windows registry access
 | 
			
		||||
winapi = { version = "0.3.9", features = ["winuser", "windef", "commdlg", "shellapi"] }
 | 
			
		||||
 | 
			
		||||
[target.'cfg(unix)'.dependencies]
 | 
			
		||||
# Linux-specific dependencies if needed
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								rust/assets/installerico.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								rust/assets/installerico.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 220 KiB  | 
							
								
								
									
										246
									
								
								rust/src/app.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								rust/src/app.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,246 @@
 | 
			
		||||
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 completed: bool,
 | 
			
		||||
    pub files: Option<Vec<PathBuf>>,
 | 
			
		||||
    pub error: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 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,
 | 
			
		||||
            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() {
 | 
			
		||||
                self.download_progress = state.progress;
 | 
			
		||||
                
 | 
			
		||||
                // if state.completed {
 | 
			
		||||
                //     if let Some(files) = &state.files {
 | 
			
		||||
                //         // Store the files for installation
 | 
			
		||||
                //         //self.next_state(); // Proceed to installation
 | 
			
		||||
                //         //self.start_installation();
 | 
			
		||||
                //     }
 | 
			
		||||
                // }
 | 
			
		||||
                
 | 
			
		||||
                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,
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        _ => 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();
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if self.state != InstallerState::GameSelection {
 | 
			
		||||
                            if ui.button("Back").clicked() {
 | 
			
		||||
                                self.previous_state();
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Implementation of key installer functions
 | 
			
		||||
impl SinfarInstallerApp<> {
 | 
			
		||||
    pub fn start_download(&mut self) {
 | 
			
		||||
 | 
			
		||||
        // let temp_dir = std::env::temp_dir().join("sinfar_installer");
 | 
			
		||||
        // println!("{}", temp_dir.display())
 | 
			
		||||
 | 
			
		||||
        // Reset progress and errors
 | 
			
		||||
        self.download_progress = 0.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| {
 | 
			
		||||
                // Update progress through shared state
 | 
			
		||||
                if let Ok(mut state) = progress_state.lock() {
 | 
			
		||||
                    state.progress = progress;
 | 
			
		||||
                    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 start_installation(&mut self) {
 | 
			
		||||
        
 | 
			
		||||
   
 | 
			
		||||
        // This would be spawned in a separate thread
 | 
			
		||||
        //let game_type = self.game_type.clone().unwrap();
 | 
			
		||||
        //let install_path = self.install_path.clone().unwrap();
 | 
			
		||||
        //let ee_exe_path = self.ee_exe_path.clone();
 | 
			
		||||
        
 | 
			
		||||
        // Implementation with sevenz-rust would go here
 | 
			
		||||
        // Update self.extraction_progress as files are extracted
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										242
									
								
								rust/src/installer/downloader.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								rust/src/installer/downloader.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,242 @@
 | 
			
		||||
// src/installer/downloader.rs
 | 
			
		||||
use anyhow::{Result, Context, anyhow};
 | 
			
		||||
use reqwest::Client;
 | 
			
		||||
use tokio::io::AsyncWriteExt;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
use futures_util::StreamExt;
 | 
			
		||||
use tokio::sync::Mutex;
 | 
			
		||||
use std::fs::metadata;
 | 
			
		||||
use reqwest::header::{HeaderMap, HeaderValue, RANGE, USER_AGENT};
 | 
			
		||||
use log::{info, warn, error};
 | 
			
		||||
use tokio::fs::File;
 | 
			
		||||
use crate::app::{GameType};
 | 
			
		||||
 | 
			
		||||
pub struct DownloadManager {
 | 
			
		||||
    client: Client,
 | 
			
		||||
    progress_callback: Arc<Mutex<Box<dyn Fn(f32, &str) + Send + Sync>>>,
 | 
			
		||||
    temp_dir: PathBuf,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DownloadManager {
 | 
			
		||||
    pub fn new<F>(progress_callback: F) -> Self 
 | 
			
		||||
    where 
 | 
			
		||||
        F: Fn(f32, &str) + Send + Sync + 'static 
 | 
			
		||||
    {
 | 
			
		||||
        let client = Client::builder()
 | 
			
		||||
            .timeout(Duration::from_secs(1800))
 | 
			
		||||
            .build()
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
            
 | 
			
		||||
        let temp_dir = std::env::temp_dir().join("sinfar_installer");
 | 
			
		||||
        println!("Files will be downloaded to: {}", temp_dir.display());
 | 
			
		||||
        // Ensure temp directory exists
 | 
			
		||||
        if !temp_dir.exists() {
 | 
			
		||||
            if let Err(e) = std::fs::create_dir_all(&temp_dir) {
 | 
			
		||||
                warn!("Failed to create temp directory: {}", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Self {
 | 
			
		||||
            client,
 | 
			
		||||
            progress_callback: Arc::new(Mutex::new(Box::new(progress_callback))),
 | 
			
		||||
            temp_dir,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Downloads a file with resume capability and progress tracking
 | 
			
		||||
    pub async fn download_file(&self, url: &str, output_path: &Path) -> Result<()> {
 | 
			
		||||
        info!("Starting download of {} to {}", url, output_path.display());
 | 
			
		||||
        
 | 
			
		||||
        // Create parent directories if they don't exist
 | 
			
		||||
        if let Some(parent) = output_path.parent() {
 | 
			
		||||
            if !parent.exists() {
 | 
			
		||||
                tokio::fs::create_dir_all(parent).await?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Check if we can resume a previous download
 | 
			
		||||
        let mut headers = HeaderMap::new();
 | 
			
		||||
        headers.insert(USER_AGENT, HeaderValue::from_static("Sinfar NWN Installer"));
 | 
			
		||||
        
 | 
			
		||||
        let mut downloaded_size: u64 = 0;
 | 
			
		||||
        
 | 
			
		||||
        if output_path.exists() {
 | 
			
		||||
            if let Ok(metadata) = tokio::fs::metadata(output_path).await {
 | 
			
		||||
                downloaded_size = metadata.len();
 | 
			
		||||
                if downloaded_size > 0 {
 | 
			
		||||
                    info!("Resuming download from byte {}", downloaded_size);
 | 
			
		||||
                    headers.insert(RANGE, HeaderValue::from_str(&format!("bytes={}-", downloaded_size))?);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let response = self.client.get(url)
 | 
			
		||||
            .headers(headers)
 | 
			
		||||
            .send()
 | 
			
		||||
            .await
 | 
			
		||||
            .context("Failed to send HTTP request")?;
 | 
			
		||||
            
 | 
			
		||||
        // Check if the server supports resuming
 | 
			
		||||
        let can_resume = response.status().is_success() && response.status().as_u16() == 206;
 | 
			
		||||
        let status = response.status();
 | 
			
		||||
        
 | 
			
		||||
        if !status.is_success() {
 | 
			
		||||
            return Err(anyhow!("Server returned error status: {}", status));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Get total file size
 | 
			
		||||
        let content_length = if can_resume {
 | 
			
		||||
            if let Some(content_range) = response.headers().get("content-range") {
 | 
			
		||||
                let range_str = content_range.to_str().unwrap_or("");
 | 
			
		||||
                if let Some(size_part) = range_str.split('/').nth(1) {
 | 
			
		||||
                    size_part.parse::<u64>().unwrap_or(0)
 | 
			
		||||
                } else {
 | 
			
		||||
                    response.content_length().unwrap_or(0) + downloaded_size
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                response.content_length().unwrap_or(0) + downloaded_size
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            response.content_length().unwrap_or(0)
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        info!("Download size: {} bytes", content_length);
 | 
			
		||||
        
 | 
			
		||||
        // Open file in appropriate mode (append if resuming, create/truncate if starting fresh)
 | 
			
		||||
        let mut file = if can_resume && downloaded_size > 0 {
 | 
			
		||||
            info!("Opening file for append");
 | 
			
		||||
            tokio::fs::OpenOptions::new()
 | 
			
		||||
                .write(true)
 | 
			
		||||
                .append(true)
 | 
			
		||||
                .open(output_path)
 | 
			
		||||
                .await?
 | 
			
		||||
        } else {
 | 
			
		||||
            info!("Creating new file");
 | 
			
		||||
            downloaded_size = 0;
 | 
			
		||||
            tokio::fs::File::create(output_path).await?
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        // Stream the response
 | 
			
		||||
        let mut stream = response.bytes_stream();
 | 
			
		||||
        let progress_callback = self.progress_callback.clone();
 | 
			
		||||
        let filename = output_path.file_name()
 | 
			
		||||
            .and_then(|f| f.to_str())
 | 
			
		||||
            .unwrap_or("file");
 | 
			
		||||
            
 | 
			
		||||
        let mut last_update = std::time::Instant::now();
 | 
			
		||||
        
 | 
			
		||||
        while let Some(chunk_result) = stream.next().await {
 | 
			
		||||
            let chunk = chunk_result?;
 | 
			
		||||
            file.write_all(&chunk).await?;
 | 
			
		||||
            
 | 
			
		||||
            downloaded_size += chunk.len() as u64;
 | 
			
		||||
            
 | 
			
		||||
            // Only update progress every 100ms to avoid overwhelming the UI
 | 
			
		||||
            let now = std::time::Instant::now();
 | 
			
		||||
            if now.duration_since(last_update) > Duration::from_millis(100) || content_length == downloaded_size {
 | 
			
		||||
                if content_length > 0 {
 | 
			
		||||
                    let progress = downloaded_size as f32 / content_length as f32;
 | 
			
		||||
                    let callback = progress_callback.lock().await;
 | 
			
		||||
                    callback(progress, filename);
 | 
			
		||||
                }
 | 
			
		||||
                last_update = now;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        info!("Download completed: {}", output_path.display());
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Downloads all required files for the installation
 | 
			
		||||
    pub async fn download_all_files(&self, game_type: &GameType) -> Result<Vec<PathBuf>> {
 | 
			
		||||
        let mut downloaded_files = Vec::new();
 | 
			
		||||
        
 | 
			
		||||
        // Always download the content files
 | 
			
		||||
        let content_zip_path = self.temp_dir.join("sinfar_all_files_v30.7z");
 | 
			
		||||
        
 | 
			
		||||
        // Check if file already exists and is of expected size
 | 
			
		||||
        if !self.file_exists_with_size(&content_zip_path, 100_000_000) {
 | 
			
		||||
            // File doesn't exist or is too small, download it
 | 
			
		||||
            self.download_file(
 | 
			
		||||
                "https://sinfar.net/haks/sinfar_all_files_v30.7z", 
 | 
			
		||||
                &content_zip_path
 | 
			
		||||
            ).await?;
 | 
			
		||||
        } else {
 | 
			
		||||
            // File exists, update progress to 100%
 | 
			
		||||
            let callback = self.progress_callback.lock().await;
 | 
			
		||||
            callback(1.0, "sinfar_all_files_v30.7z");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        downloaded_files.push(content_zip_path);
 | 
			
		||||
        
 | 
			
		||||
        // Download version-specific files
 | 
			
		||||
        if *game_type == GameType::Diamond {
 | 
			
		||||
            let launcher_path = self.temp_dir.join("sinfarx.exe");
 | 
			
		||||
            
 | 
			
		||||
            if !self.file_exists_with_size(&launcher_path, 10_000) {
 | 
			
		||||
                self.download_file(
 | 
			
		||||
                    "https://nwn.sinfar.net/files/sinfarx.exe", 
 | 
			
		||||
                    &launcher_path
 | 
			
		||||
                ).await?;
 | 
			
		||||
            } else {
 | 
			
		||||
                let callback = self.progress_callback.lock().await;
 | 
			
		||||
                callback(1.0, "sinfarx.exe");
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            downloaded_files.push(launcher_path);
 | 
			
		||||
        } else if *game_type == GameType::EnhancedEdition {
 | 
			
		||||
            // For EE, download the appropriate launcher for the platform
 | 
			
		||||
            #[cfg(windows)]
 | 
			
		||||
            {
 | 
			
		||||
                let launcher_zip_path = self.temp_dir.join("sinfarLauncher.zip");
 | 
			
		||||
                
 | 
			
		||||
                if !self.file_exists_with_size(&launcher_zip_path, 10_000) {
 | 
			
		||||
                    self.download_file(
 | 
			
		||||
                        "https://nwn.sinfar.net/files/sinfarx/8181/win32_8181.zip", 
 | 
			
		||||
                        &launcher_zip_path
 | 
			
		||||
                    ).await?;
 | 
			
		||||
                } else {
 | 
			
		||||
                    let callback = self.progress_callback.lock().await;
 | 
			
		||||
                    callback(1.0, "sinfarLauncher.zip");
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                downloaded_files.push(launcher_zip_path);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            #[cfg(unix)]
 | 
			
		||||
            {
 | 
			
		||||
                let launcher_zip_path = self.temp_dir.join("sinfarLauncher.zip");
 | 
			
		||||
                
 | 
			
		||||
                if !self.file_exists_with_size(&launcher_zip_path, 10_000) {
 | 
			
		||||
                    self.download_file(
 | 
			
		||||
                        "https://nwn.sinfar.net/files/sinfarx/8181/linux_8181.zip", 
 | 
			
		||||
                        &launcher_zip_path
 | 
			
		||||
                    ).await?;
 | 
			
		||||
                } else {
 | 
			
		||||
                    let callback = self.progress_callback.lock().await;
 | 
			
		||||
                    callback(1.0, "sinfarLauncher.zip");
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                downloaded_files.push(launcher_zip_path);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(downloaded_files)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Check if a file exists and meets a minimum size requirement
 | 
			
		||||
    fn file_exists_with_size(&self, path: &Path, min_size: u64) -> bool {
 | 
			
		||||
        if let Ok(meta) = metadata(path) {
 | 
			
		||||
            meta.is_file() && meta.len() >= min_size
 | 
			
		||||
        } else {
 | 
			
		||||
            false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Get the path to the temporary directory
 | 
			
		||||
    pub fn get_temp_dir(&self) -> &Path {
 | 
			
		||||
        &self.temp_dir
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										190
									
								
								rust/src/installer/extractor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								rust/src/installer/extractor.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,190 @@
 | 
			
		||||
// src/installer/extractor.rs
 | 
			
		||||
use anyhow::{Result, Context};
 | 
			
		||||
use sevenz_rust::{Archive, BlockDecoder, decompress};
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use tokio::sync::Mutex;
 | 
			
		||||
use log::{info, warn, error};
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::io::{Read, Write};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
pub struct ExtractionManager {
 | 
			
		||||
    progress_callback: Arc<Mutex<Box<dyn Fn(f32, &str) + Send + Sync>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ExtractionManager {
 | 
			
		||||
    pub fn new<F>(progress_callback: F) -> Self 
 | 
			
		||||
    where 
 | 
			
		||||
        F: Fn(f32, &str) + Send + Sync + 'static 
 | 
			
		||||
    {
 | 
			
		||||
        Self {
 | 
			
		||||
            progress_callback: Arc::new(Mutex::new(Box::new(progress_callback))),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Extract a 7z archive with progress reporting
 | 
			
		||||
    pub async fn extract_7z(&self, archive_path: &Path, output_dir: &Path) -> Result<()> {
 | 
			
		||||
        info!("Extracting 7z archive: {} -> {}", archive_path.display(), output_dir.display());
 | 
			
		||||
        
 | 
			
		||||
        // Make sure the output directory exists
 | 
			
		||||
        if !output_dir.exists() {
 | 
			
		||||
            tokio::fs::create_dir_all(output_dir).await?;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Get file size for progress calculation
 | 
			
		||||
        let file_size = fs::metadata(archive_path)?.len();
 | 
			
		||||
        
 | 
			
		||||
        // We'll handle extraction in a blocking task since sevenz-rust is synchronous
 | 
			
		||||
        let archive_path = archive_path.to_path_buf();
 | 
			
		||||
        let output_dir = output_dir.to_path_buf();
 | 
			
		||||
        let progress_callback = self.progress_callback.clone();
 | 
			
		||||
        
 | 
			
		||||
        tokio::task::spawn_blocking(move || -> Result<()> {
 | 
			
		||||
            let archive_file = fs::File::open(&archive_path)?;
 | 
			
		||||
            let out = fs::create_dir_all(&output_dir)?;
 | 
			
		||||
            
 | 
			
		||||
            // decompress takes a Read + Seek and a destination directory
 | 
			
		||||
            decompress(&archive_file, &output_dir)?;
 | 
			
		||||
        
 | 
			
		||||
            // Call progress update with 100% after decompression
 | 
			
		||||
            let filename = archive_path.file_name()
 | 
			
		||||
                .and_then(|f| f.to_str())
 | 
			
		||||
                .unwrap_or("archive");
 | 
			
		||||
        
 | 
			
		||||
            let progress_callback_clone = progress_callback.clone();
 | 
			
		||||
            tokio::runtime::Handle::current().block_on(async move {
 | 
			
		||||
                let callback = progress_callback_clone.lock().await;
 | 
			
		||||
                callback(1.0, filename); // 100% done
 | 
			
		||||
            });
 | 
			
		||||
        
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }).await??;
 | 
			
		||||
        
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Extract a ZIP archive with progress reporting
 | 
			
		||||
    pub async fn extract_zip(&self, archive_path: &Path, output_dir: &Path) -> Result<()> {
 | 
			
		||||
        info!("Extracting ZIP archive: {} -> {}", archive_path.display(), output_dir.display());
 | 
			
		||||
        
 | 
			
		||||
        // Make sure the output directory exists
 | 
			
		||||
        if !output_dir.exists() {
 | 
			
		||||
            tokio::fs::create_dir_all(output_dir).await?;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // We'll handle extraction in a blocking task since the zip crate is synchronous
 | 
			
		||||
        let archive_path = archive_path.to_path_buf();
 | 
			
		||||
        let output_dir = output_dir.to_path_buf();
 | 
			
		||||
        let progress_callback = self.progress_callback.clone();
 | 
			
		||||
        
 | 
			
		||||
        tokio::task::spawn_blocking(move || -> Result<()> {
 | 
			
		||||
            let file = fs::File::open(&archive_path)?;
 | 
			
		||||
            let mut archive = zip::ZipArchive::new(file)?;
 | 
			
		||||
            
 | 
			
		||||
            let total_entries = archive.len();
 | 
			
		||||
            info!("ZIP archive contains {} entries", total_entries);
 | 
			
		||||
            
 | 
			
		||||
            for i in 0..total_entries {
 | 
			
		||||
                let mut file = archive.by_index(i)?;
 | 
			
		||||
                let outpath = match file.enclosed_name() {
 | 
			
		||||
                    Some(path) => output_dir.join(path),
 | 
			
		||||
                    None => continue,
 | 
			
		||||
                };
 | 
			
		||||
                
 | 
			
		||||
                // Update progress
 | 
			
		||||
                let progress = (i + 1) as f32 / total_entries as f32;
 | 
			
		||||
                let filename = outpath.file_name()
 | 
			
		||||
                    .and_then(|f| f.to_str())
 | 
			
		||||
                    .unwrap_or("unknown");
 | 
			
		||||
                
 | 
			
		||||
                if (*file.name()).ends_with('/') {
 | 
			
		||||
                    fs::create_dir_all(&outpath)?;
 | 
			
		||||
                } else {
 | 
			
		||||
                    if let Some(p) = outpath.parent() {
 | 
			
		||||
                        if !p.exists() {
 | 
			
		||||
                            fs::create_dir_all(p)?;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    let mut outfile = fs::File::create(&outpath)?;
 | 
			
		||||
                    std::io::copy(&mut file, &mut outfile)?;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Fix file permissions on Unix
 | 
			
		||||
                #[cfg(unix)]
 | 
			
		||||
                {
 | 
			
		||||
                    use std::os::unix::fs::PermissionsExt;
 | 
			
		||||
                    if let Some(mode) = file.unix_mode() {
 | 
			
		||||
                        fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Update progress on the tokio runtime
 | 
			
		||||
                let progress_callback_clone = progress_callback.clone();
 | 
			
		||||
                tokio::runtime::Handle::current().block_on(async move {
 | 
			
		||||
                    let callback = progress_callback_clone.lock().await;
 | 
			
		||||
                    callback(progress, filename);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            info!("ZIP extraction completed successfully");
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }).await??;
 | 
			
		||||
        
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Handles extraction of all downloaded files to their appropriate locations
 | 
			
		||||
    pub async fn install_all_files(
 | 
			
		||||
        &self, 
 | 
			
		||||
        downloaded_files: &[PathBuf], 
 | 
			
		||||
        install_path: &Path, 
 | 
			
		||||
        game_type: &str, 
 | 
			
		||||
        ee_exe_path: Option<&Path>
 | 
			
		||||
    ) -> Result<()> {
 | 
			
		||||
        // Find and extract the main content files
 | 
			
		||||
        let content_archive = downloaded_files.iter()
 | 
			
		||||
            .find(|p| p.file_name().map_or(false, |f| f.to_str().unwrap_or("").contains("sinfar_all_files")))
 | 
			
		||||
            .context("Content archive not found in downloaded files")?;
 | 
			
		||||
            
 | 
			
		||||
        // Extract main content
 | 
			
		||||
        self.extract_7z(content_archive, install_path).await?;
 | 
			
		||||
        
 | 
			
		||||
        // Handle game-specific files
 | 
			
		||||
        if game_type.to_lowercase() == "diamond" {
 | 
			
		||||
            // For Diamond, find and copy the launcher
 | 
			
		||||
            let launcher = downloaded_files.iter()
 | 
			
		||||
                .find(|p| p.file_name().map_or(false, |f| f.to_str().unwrap_or("").contains("sinfarx.exe")))
 | 
			
		||||
                .context("Launcher executable not found in downloaded files")?;
 | 
			
		||||
                
 | 
			
		||||
            // Copy launcher to install directory
 | 
			
		||||
            let target_path = install_path.join("sinfarx.exe");
 | 
			
		||||
            tokio::fs::copy(launcher, &target_path).await?;
 | 
			
		||||
            
 | 
			
		||||
        } else if game_type.to_lowercase() == "enhancededition" || game_type.to_lowercase() == "ee" {
 | 
			
		||||
            // For EE, find the launcher zip
 | 
			
		||||
            let launcher_zip = downloaded_files.iter()
 | 
			
		||||
                .find(|p| p.file_name().map_or(false, |f| f.to_str().unwrap_or("").contains("sinfarLauncher.zip")))
 | 
			
		||||
                .context("Launcher ZIP not found in downloaded files")?;
 | 
			
		||||
                
 | 
			
		||||
            // Extract to the EE executable directory
 | 
			
		||||
            if let Some(ee_path) = ee_exe_path {
 | 
			
		||||
                // Create the bin/win32_8181 directory if it doesn't exist (Windows)
 | 
			
		||||
                // or bin/linux_8181 directory (Linux)
 | 
			
		||||
                #[cfg(windows)]
 | 
			
		||||
                let extract_dir = ee_path.join("bin").join("win32_8181");
 | 
			
		||||
                
 | 
			
		||||
                #[cfg(unix)]
 | 
			
		||||
                let extract_dir = ee_path.join("bin").join("linux_8181");
 | 
			
		||||
                
 | 
			
		||||
                // Extract the launcher
 | 
			
		||||
                self.extract_zip(launcher_zip, &extract_dir).await?;
 | 
			
		||||
            } else {
 | 
			
		||||
                return Err(anyhow::anyhow!("EE executable path is required for EE installation"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        info!("All files installed successfully");
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								rust/src/installer/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								rust/src/installer/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
pub mod downloader;
 | 
			
		||||
pub mod downloaderv2;
 | 
			
		||||
pub mod extractor;
 | 
			
		||||
pub mod validator;
 | 
			
		||||
pub mod shortcut;
 | 
			
		||||
							
								
								
									
										64
									
								
								rust/src/installer/shortcut.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								rust/src/installer/shortcut.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
pub mod shortcut {
 | 
			
		||||
    use std::path::{Path, PathBuf};
 | 
			
		||||
    use std::io::Write;
 | 
			
		||||
    use anyhow::Result;
 | 
			
		||||
 | 
			
		||||
    pub fn create_shortcut(
 | 
			
		||||
        target_path: &Path,
 | 
			
		||||
        shortcut_name: &str,
 | 
			
		||||
        icon_path: Option<&Path>,
 | 
			
		||||
    ) -> Result<()> {
 | 
			
		||||
        #[cfg(windows)]
 | 
			
		||||
        {
 | 
			
		||||
            use std::os::windows::process::CommandExt;
 | 
			
		||||
            use std::process::Command;
 | 
			
		||||
            
 | 
			
		||||
            let desktop_path = dirs::desktop_dir().unwrap_or_else(|| PathBuf::from("."));
 | 
			
		||||
            let shortcut_path = desktop_path.join(format!("{}.lnk", shortcut_name));
 | 
			
		||||
            
 | 
			
		||||
            let powershell_command = format!(
 | 
			
		||||
                "$s = (New-Object -ComObject WScript.Shell).CreateShortcut('{}'); $s.TargetPath = '{}'; {}; $s.Save()",
 | 
			
		||||
                shortcut_path.to_string_lossy(),
 | 
			
		||||
                target_path.to_string_lossy(),
 | 
			
		||||
                if let Some(icon) = icon_path {
 | 
			
		||||
                    format!("$s.IconLocation = '{}'", icon.to_string_lossy())
 | 
			
		||||
                } else {
 | 
			
		||||
                    String::new()
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            Command::new("powershell")
 | 
			
		||||
                .args(["-Command", &powershell_command])
 | 
			
		||||
                .creation_flags(0x08000000) // CREATE_NO_WINDOW
 | 
			
		||||
                .output()?;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        #[cfg(unix)]
 | 
			
		||||
        {
 | 
			
		||||
            let desktop_path = dirs::desktop_dir().unwrap_or_else(|| PathBuf::from("."));
 | 
			
		||||
            let shortcut_path = desktop_path.join(format!("{}.desktop", shortcut_name));
 | 
			
		||||
            
 | 
			
		||||
            let mut desktop_file = std::fs::File::create(shortcut_path)?;
 | 
			
		||||
            
 | 
			
		||||
            writeln!(desktop_file, "[Desktop Entry]")?;
 | 
			
		||||
            writeln!(desktop_file, "Type=Application")?;
 | 
			
		||||
            writeln!(desktop_file, "Name={}", shortcut_name)?;
 | 
			
		||||
            writeln!(desktop_file, "Exec={}", target_path.to_string_lossy())?;
 | 
			
		||||
            if let Some(icon) = icon_path {
 | 
			
		||||
                writeln!(desktop_file, "Icon={}", icon.to_string_lossy())?;
 | 
			
		||||
            }
 | 
			
		||||
            writeln!(desktop_file, "Terminal=false")?;
 | 
			
		||||
            
 | 
			
		||||
            // Make the .desktop file executable
 | 
			
		||||
            #[cfg(unix)]
 | 
			
		||||
            {
 | 
			
		||||
                use std::os::unix::fs::PermissionsExt;
 | 
			
		||||
                let mut perms = std::fs::metadata(&shortcut_path)?.permissions();
 | 
			
		||||
                perms.set_mode(0o755);
 | 
			
		||||
                std::fs::set_permissions(&shortcut_path, perms)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								rust/src/installer/validator.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								rust/src/installer/validator.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
pub mod validator {
 | 
			
		||||
    use std::path::Path;
 | 
			
		||||
 | 
			
		||||
    pub fn validate_diamond_path(path: &Path) -> bool {
 | 
			
		||||
        path.join("tlk").exists() && path.join("hak").exists()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn validate_ee_path(path: &Path) -> bool {
 | 
			
		||||
        path.join("tlk").exists() && path.join("hak").exists()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn validate_ee_exe_path(path: &Path) -> bool {
 | 
			
		||||
        #[cfg(windows)]
 | 
			
		||||
        return path.join("bin").join("win32").exists();
 | 
			
		||||
        
 | 
			
		||||
        #[cfg(unix)]
 | 
			
		||||
        return path.join("bin").join("linux-x86").exists() || path.join("bin").join("linux-x86_64").exists();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								rust/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								rust/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
use eframe::{NativeOptions, run_native, App};
 | 
			
		||||
use log::info;
 | 
			
		||||
use tokio::runtime::Runtime;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use eframe::IconData;
 | 
			
		||||
 | 
			
		||||
mod app;
 | 
			
		||||
mod ui;
 | 
			
		||||
mod installer;
 | 
			
		||||
//mod util;
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    env_logger::init();
 | 
			
		||||
    info!("Starting Sinfar NWN Custom Content Installer");
 | 
			
		||||
 | 
			
		||||
    let rt = Runtime::new().expect("Failed to create Tokio runtime");
 | 
			
		||||
    let rt = Arc::new(rt);
 | 
			
		||||
 | 
			
		||||
    let options = NativeOptions {
 | 
			
		||||
        initial_window_size: Some(eframe::egui::vec2(600.0, 400.0)),
 | 
			
		||||
        resizable: false,
 | 
			
		||||
        icon_data: load_icon(),
 | 
			
		||||
        ..Default::default()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let app_rt = rt.clone();
 | 
			
		||||
    run_native(
 | 
			
		||||
        "Sinfar NWN Custom Content Installer",
 | 
			
		||||
        options,
 | 
			
		||||
        Box::new(move |cc| Box::new(app::SinfarInstallerApp::new(cc, app_rt.clone()))),
 | 
			
		||||
    ).expect("Failed to start application");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn load_icon() -> Option<eframe::IconData> {
 | 
			
		||||
    #[cfg(feature = "embed_icon")]
 | 
			
		||||
    {
 | 
			
		||||
        let icon_bytes = include_bytes!("../assets/installerico.ico");
 | 
			
		||||
        // Convert ico to rgba data
 | 
			
		||||
        match image::load_from_memory(icon_bytes) {
 | 
			
		||||
            Ok(image) => {
 | 
			
		||||
                let image_rgba = image.to_rgba8();
 | 
			
		||||
                let (width, height) = image_rgba.dimensions();
 | 
			
		||||
                let rgba = image_rgba.into_raw();
 | 
			
		||||
                Some(eframe::IconData { rgba, width, height })
 | 
			
		||||
            },
 | 
			
		||||
            Err(_) => None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    #[cfg(not(feature = "embed_icon"))]
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								rust/src/ui/download_progress.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								rust/src/ui/download_progress.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
pub mod download_progress {
 | 
			
		||||
    use eframe::egui::{Ui, ProgressBar};
 | 
			
		||||
    use crate::app::SinfarInstallerApp;
 | 
			
		||||
 | 
			
		||||
    pub fn render(ui: &mut Ui, app: &mut SinfarInstallerApp) {
 | 
			
		||||
        ui.heading("Downloading Sinfar Custom Content");
 | 
			
		||||
        ui.add_space(10.0);
 | 
			
		||||
        
 | 
			
		||||
        ui.horizontal(|ui| {
 | 
			
		||||
            ui.label("Downloading:");
 | 
			
		||||
            ui.add(ProgressBar::new(app.download_progress).show_percentage());
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        if let Some(error) = &app.download_error {
 | 
			
		||||
            ui.colored_label(eframe::egui::Color32::RED, format!("Error: {}", error));
 | 
			
		||||
            if ui.button("Retry").clicked() {
 | 
			
		||||
                app.download_error = None;
 | 
			
		||||
                app.download_progress = 0.0;
 | 
			
		||||
                app.start_download();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								rust/src/ui/game_selection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								rust/src/ui/game_selection.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
pub mod game_selection {
 | 
			
		||||
    use eframe::egui::Ui;
 | 
			
		||||
    use crate::app::{SinfarInstallerApp, GameType};
 | 
			
		||||
    use std::sync::Once;
 | 
			
		||||
 | 
			
		||||
    static INIT: Once = Once::new();
 | 
			
		||||
 | 
			
		||||
    pub fn render(ui: &mut Ui, app: &mut SinfarInstallerApp) {
 | 
			
		||||
        INIT.call_once(|| {
 | 
			
		||||
            print_selected_game_type(&app.game_type);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        ui.heading("Select your game's version:");
 | 
			
		||||
        ui.add_space(10.0);
 | 
			
		||||
 | 
			
		||||
        ui.vertical(|ui| {
 | 
			
		||||
            if ui.radio_value(&mut app.game_type, Some(GameType::Diamond), "Neverwinter Nights Diamond Edition").clicked() {
 | 
			
		||||
                // Handle selection
 | 
			
		||||
                print_selected_game_type(&app.game_type);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if ui.radio_value(&mut app.game_type, Some(GameType::EnhancedEdition), "Neverwinter Nights: Enhanced Edition").clicked() {
 | 
			
		||||
                // Handle selection
 | 
			
		||||
                print_selected_game_type(&app.game_type);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn print_selected_game_type(game_type: &Option<GameType>){
 | 
			
		||||
        match game_type {
 | 
			
		||||
            Some(game_type_ref) => match game_type_ref {
 | 
			
		||||
                GameType::Diamond => println!("Diamond Edition selected"),
 | 
			
		||||
                GameType::EnhancedEdition => println!("Enhanced Edition selected"),
 | 
			
		||||
            },
 | 
			
		||||
            None => println!("No game selected. (If you see this.. you fucked up.)"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								rust/src/ui/installation_progress.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								rust/src/ui/installation_progress.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
pub mod installation_progress {
 | 
			
		||||
    use eframe::egui::{Ui, ProgressBar};
 | 
			
		||||
    use crate::app::SinfarInstallerApp;
 | 
			
		||||
 | 
			
		||||
    pub fn render(ui: &mut Ui, app: &mut SinfarInstallerApp) {
 | 
			
		||||
        ui.heading("Installing Sinfar Custom Content");
 | 
			
		||||
        ui.add_space(10.0);
 | 
			
		||||
        
 | 
			
		||||
        ui.horizontal(|ui| {
 | 
			
		||||
            ui.label("Extracting files:");
 | 
			
		||||
            ui.add(ProgressBar::new(app.extraction_progress).show_percentage());
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        if let Some(error) = &app.install_error {
 | 
			
		||||
            ui.colored_label(eframe::egui::Color32::RED, error);
 | 
			
		||||
            if ui.button("Retry").clicked() {
 | 
			
		||||
                app.install_error = None;
 | 
			
		||||
                app.start_installation();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								rust/src/ui/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								rust/src/ui/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
pub mod game_selection;
 | 
			
		||||
pub mod path_selection;
 | 
			
		||||
pub mod download_progress;
 | 
			
		||||
pub mod installation_progress;
 | 
			
		||||
							
								
								
									
										105
									
								
								rust/src/ui/path_selection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								rust/src/ui/path_selection.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,105 @@
 | 
			
		||||
pub mod path_selection {
 | 
			
		||||
    use eframe::egui::{Ui, TextEdit};
 | 
			
		||||
    use crate::app::{SinfarInstallerApp, GameType};
 | 
			
		||||
    use crate::installer::validator::*;
 | 
			
		||||
 | 
			
		||||
    use rfd::FileDialog;
 | 
			
		||||
    use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
    pub fn render(ui: &mut Ui, app: &mut SinfarInstallerApp) {
 | 
			
		||||
        ui.heading("Select installation paths");
 | 
			
		||||
        ui.add_space(10.0);
 | 
			
		||||
 | 
			
		||||
        // Set default paths if not set
 | 
			
		||||
        if app.install_path.is_none() {
 | 
			
		||||
            app.install_path = Some(default_install_path(&app.game_type.clone().unwrap_or(GameType::Diamond)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if app.game_type == Some(GameType::EnhancedEdition) && app.ee_exe_path.is_none() {
 | 
			
		||||
            app.ee_exe_path = Some(default_ee_exe_path());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ui.label("Please select your Neverwinter Nights install folder:");
 | 
			
		||||
        
 | 
			
		||||
        ui.horizontal(|ui| {
 | 
			
		||||
            let mut path_str = app.install_path.clone().unwrap_or_default().to_string_lossy().to_string();
 | 
			
		||||
            let response = ui.add(TextEdit::singleline(&mut path_str).desired_width(ui.available_width() - 80.0));
 | 
			
		||||
            if response.changed() {
 | 
			
		||||
                app.install_path = Some(PathBuf::from(path_str));
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if ui.button("Browse...").clicked() {
 | 
			
		||||
                if let Some(path) = FileDialog::new()
 | 
			
		||||
                    .set_title("Select Neverwinter Nights Folder")
 | 
			
		||||
                    .set_directory(app.install_path.clone().unwrap_or_default())
 | 
			
		||||
                    .pick_folder() {
 | 
			
		||||
                    app.install_path = Some(path);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let is_content_path_valid = match &app.game_type {
 | 
			
		||||
            Some(GameType::Diamond) => validator::validate_diamond_path(app.install_path.as_ref().unwrap()),
 | 
			
		||||
            Some(GameType::EnhancedEdition) => validator::validate_ee_path(app.install_path.as_ref().unwrap()),
 | 
			
		||||
            None => false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if !is_content_path_valid {
 | 
			
		||||
            ui.colored_label(eframe::egui::Color32::RED, "Invalid path: The selected folder must contain both 'tlk' and 'hak' subfolders.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Only show EE path selection if EE is selected
 | 
			
		||||
        if app.game_type == Some(GameType::EnhancedEdition) {
 | 
			
		||||
            ui.add_space(10.0);
 | 
			
		||||
            ui.label("Select the location of the nwn executable for Enhanced Edition:");
 | 
			
		||||
            
 | 
			
		||||
            ui.horizontal(|ui| {
 | 
			
		||||
                let mut ee_path_str = app.ee_exe_path.clone().unwrap_or_default().to_string_lossy().to_string();
 | 
			
		||||
                let response = ui.add(TextEdit::singleline(&mut ee_path_str).desired_width(ui.available_width() - 80.0));
 | 
			
		||||
                if response.changed() {
 | 
			
		||||
                    app.ee_exe_path = Some(PathBuf::from(ee_path_str));
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if ui.button("Browse...").clicked() {
 | 
			
		||||
                    if let Some(path) = FileDialog::new()
 | 
			
		||||
                        .set_title("Select Neverwinter Nights executable location")
 | 
			
		||||
                        .set_directory(app.ee_exe_path.clone().unwrap_or_default())
 | 
			
		||||
                        .pick_folder() {
 | 
			
		||||
                        app.ee_exe_path = Some(path);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            if !validator::validate_ee_exe_path(app.ee_exe_path.as_ref().unwrap()) {
 | 
			
		||||
                ui.colored_label(eframe::egui::Color32::RED, "Invalid path: Neverwinter Night EE not detected.");
 | 
			
		||||
            }
 | 
			
		||||
        }        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn default_install_path(game_type: &GameType) -> PathBuf {
 | 
			
		||||
        match game_type {
 | 
			
		||||
            GameType::Diamond => {
 | 
			
		||||
                #[cfg(windows)]
 | 
			
		||||
                return PathBuf::from("C:\\Program Files (x86)\\GOG Galaxy\\Games\\NWN Diamond");
 | 
			
		||||
                
 | 
			
		||||
                #[cfg(unix)]
 | 
			
		||||
                return PathBuf::from("/home").join(whoami::username()).join(".local/share/NWN Diamond");
 | 
			
		||||
            },
 | 
			
		||||
            GameType::EnhancedEdition => {
 | 
			
		||||
                #[cfg(windows)]
 | 
			
		||||
                return PathBuf::from(dirs::document_dir().unwrap_or_default()).join("Neverwinter Nights");
 | 
			
		||||
                
 | 
			
		||||
                #[cfg(unix)]
 | 
			
		||||
                return PathBuf::from("/home").join(whoami::username()).join(".local/share/Neverwinter Nights");
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn default_ee_exe_path() -> PathBuf {
 | 
			
		||||
        #[cfg(windows)]
 | 
			
		||||
        return PathBuf::from("C:\\Program Files (x86)\\GOG Galaxy\\Games\\Neverwinter Nights Enhanced Edition");
 | 
			
		||||
        
 | 
			
		||||
        #[cfg(unix)]
 | 
			
		||||
        return PathBuf::from("/home").join(whoami::username()).join(".local/share/Steam/steamapps/common/Neverwinter Nights");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								rust/src/util/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								rust/src/util/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
pub mod platform;
 | 
			
		||||
							
								
								
									
										280
									
								
								rust/src/util/platform.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								rust/src/util/platform.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,280 @@
 | 
			
		||||
// src/util/platform.rs
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
 | 
			
		||||
/// Platform-agnostic function to get the user's documents directory
 | 
			
		||||
pub fn get_documents_dir() -> Option<PathBuf> {
 | 
			
		||||
    dirs::document_dir()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get the default installation directory for NWN based on platform
 | 
			
		||||
pub fn get_default_nwn_dir(is_ee: bool) -> PathBuf {
 | 
			
		||||
    if is_ee {
 | 
			
		||||
        #[cfg(windows)]
 | 
			
		||||
        {
 | 
			
		||||
            if let Some(docs) = get_documents_dir() {
 | 
			
		||||
                return docs.join("Neverwinter Nights");
 | 
			
		||||
            } else {
 | 
			
		||||
                return PathBuf::from("C:\\Users\\Public\\Documents\\Neverwinter Nights");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        #[cfg(unix)]
 | 
			
		||||
        {
 | 
			
		||||
            if let Some(home) = dirs::home_dir() {
 | 
			
		||||
                return home.join(".local/share/Neverwinter Nights");
 | 
			
		||||
            } else {
 | 
			
		||||
                return PathBuf::from("/home").join(whoami::username()).join(".local/share/Neverwinter Nights");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        // Diamond Edition
 | 
			
		||||
        #[cfg(windows)]
 | 
			
		||||
        {
 | 
			
		||||
            // Try common installation paths for GOG and Steam
 | 
			
		||||
            let gog_path = PathBuf::from("C:\\Program Files (x86)\\GOG Galaxy\\Games\\NWN Diamond");
 | 
			
		||||
            if gog_path.exists() {
 | 
			
		||||
                return gog_path;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            let steam_path = PathBuf::from("C:\\Program Files (x86)\\Steam\\steamapps\\common\\NWN Diamond");
 | 
			
		||||
            if steam_path.exists() {
 | 
			
		||||
                return steam_path;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Default to GOG path even if it doesn't exist
 | 
			
		||||
            return gog_path;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        #[cfg(unix)]
 | 
			
		||||
        {
 | 
			
		||||
            if let Some(home) = dirs::home_dir() {
 | 
			
		||||
                // Check common Linux installation locations
 | 
			
		||||
                let steam_path = home.join(".steam/steam/steamapps/common/NWN Diamond");
 | 
			
		||||
                if steam_path.exists() {
 | 
			
		||||
                    return steam_path;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                let gog_path = home.join("GOG Games/NWN Diamond");
 | 
			
		||||
                if gog_path.exists() {
 | 
			
		||||
                    return gog_path;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                return home.join(".local/share/NWN Diamond");
 | 
			
		||||
            } else {
 | 
			
		||||
                return PathBuf::from("/home").join(whoami::username()).join(".local/share/NWN Diamond");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get the executable name with the correct extension based on platform
 | 
			
		||||
pub fn get_executable_name(base_name: &str) -> String {
 | 
			
		||||
    #[cfg(windows)]
 | 
			
		||||
    return format!("{}.exe", base_name);
 | 
			
		||||
    
 | 
			
		||||
    #[cfg(unix)]
 | 
			
		||||
    return base_name.to_string();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Check if we need elevated permissions for a path
 | 
			
		||||
pub fn needs_elevation(path: &Path) -> bool {
 | 
			
		||||
    #[cfg(windows)]
 | 
			
		||||
    {
 | 
			
		||||
        // Common paths that typically need elevation on Windows
 | 
			
		||||
        path.to_string_lossy().to_lowercase().contains("program files") || 
 | 
			
		||||
        path.to_string_lossy().to_lowercase().contains("programfiles") ||
 | 
			
		||||
        path.to_string_lossy().to_lowercase().contains("\\windows\\")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #[cfg(unix)]
 | 
			
		||||
    {
 | 
			
		||||
        // Common paths that typically need elevation on Linux
 | 
			
		||||
        path.to_string_lossy().starts_with("/usr") || 
 | 
			
		||||
        path.to_string_lossy().starts_with("/opt") ||
 | 
			
		||||
        path.to_string_lossy().starts_with("/bin") ||
 | 
			
		||||
        path.to_string_lossy().starts_with("/sbin")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Request elevated permissions
 | 
			
		||||
pub fn request_elevation() -> Result<bool> {
 | 
			
		||||
    #[cfg(windows)]
 | 
			
		||||
    {
 | 
			
		||||
        use std::process::Command;
 | 
			
		||||
        use std::env;
 | 
			
		||||
        
 | 
			
		||||
        let executable = env::current_exe()?;
 | 
			
		||||
        
 | 
			
		||||
        // Don't try to re-elevate if we're already elevated
 | 
			
		||||
        if is_elevated()? {
 | 
			
		||||
            return Ok(true);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Use the Windows ShellExecute to prompt for elevation
 | 
			
		||||
        let result = Command::new("powershell")
 | 
			
		||||
            .args(&[
 | 
			
		||||
                "-Command",
 | 
			
		||||
                &format!("Start-Process -FilePath '{}' -Verb RunAs", executable.to_string_lossy()),
 | 
			
		||||
            ])
 | 
			
		||||
            .output()?;
 | 
			
		||||
            
 | 
			
		||||
        // If the command executed successfully, exit the current non-elevated process
 | 
			
		||||
        if result.status.success() {
 | 
			
		||||
            std::process::exit(0);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(false)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #[cfg(unix)]
 | 
			
		||||
    {
 | 
			
		||||
        use std::process::Command;
 | 
			
		||||
        use std::env;
 | 
			
		||||
        
 | 
			
		||||
        let executable = env::current_exe()?;
 | 
			
		||||
        
 | 
			
		||||
        // Don't try to re-elevate if we're already elevated
 | 
			
		||||
        if is_elevated()? {
 | 
			
		||||
            return Ok(true);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Try pkexec first (GNOME/KDE)
 | 
			
		||||
        if Command::new("which").arg("pkexec").output()?.status.success() {
 | 
			
		||||
            let _ = Command::new("pkexec")
 | 
			
		||||
                .arg(executable)
 | 
			
		||||
                .spawn()?;
 | 
			
		||||
            std::process::exit(0);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Try gksudo next (older GNOME)
 | 
			
		||||
        if Command::new("which").arg("gksudo").output()?.status.success() {
 | 
			
		||||
            let _ = Command::new("gksudo")
 | 
			
		||||
                .arg("--")
 | 
			
		||||
                .arg(executable)
 | 
			
		||||
                .spawn()?;
 | 
			
		||||
            std::process::exit(0);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Try kdesudo (KDE)
 | 
			
		||||
        if Command::new("which").arg("kdesudo").output()?.status.success() {
 | 
			
		||||
            let _ = Command::new("kdesudo")
 | 
			
		||||
                .arg("--")
 | 
			
		||||
                .arg(executable)
 | 
			
		||||
                .spawn()?;
 | 
			
		||||
            std::process::exit(0);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Fallback to terminal-based sudo
 | 
			
		||||
        if Command::new("which").arg("sudo").output()?.status.success() {
 | 
			
		||||
            let _ = Command::new("sudo")
 | 
			
		||||
                .arg(executable)
 | 
			
		||||
                .spawn()?;
 | 
			
		||||
            std::process::exit(0);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(false)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Check if the current process has elevated permissions
 | 
			
		||||
pub fn is_elevated() -> Result<bool> {
 | 
			
		||||
    #[cfg(windows)]
 | 
			
		||||
    {
 | 
			
		||||
        use std::process::Command;
 | 
			
		||||
        
 | 
			
		||||
        // This command succeeds only if the process has admin privileges
 | 
			
		||||
        let output = Command::new("net")
 | 
			
		||||
            .args(&["session"])
 | 
			
		||||
            .output()?;
 | 
			
		||||
            
 | 
			
		||||
        Ok(output.status.success())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #[cfg(unix)]
 | 
			
		||||
    {
 | 
			
		||||
        // On Unix-like systems, check if we're root (uid 0)
 | 
			
		||||
        Ok(unsafe { libc::geteuid() == 0 })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create a desktop shortcut in a platform-specific way
 | 
			
		||||
pub fn create_desktop_shortcut(
 | 
			
		||||
    target: &Path, 
 | 
			
		||||
    name: &str, 
 | 
			
		||||
    icon: Option<&Path>,
 | 
			
		||||
    description: Option<&str>
 | 
			
		||||
) -> Result<()> {
 | 
			
		||||
    #[cfg(windows)]
 | 
			
		||||
    {
 | 
			
		||||
        use std::process::Command;
 | 
			
		||||
        
 | 
			
		||||
        let desktop = if let Some(desktop_dir) = dirs::desktop_dir() {
 | 
			
		||||
            desktop_dir
 | 
			
		||||
        } else {
 | 
			
		||||
            return Err(anyhow::anyhow!("Could not find desktop directory"));
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        let shortcut_path = desktop.join(format!("{}.lnk", name));
 | 
			
		||||
        
 | 
			
		||||
        // Create Windows shortcut using PowerShell
 | 
			
		||||
        let mut ps_cmd = String::from("$ws = New-Object -ComObject WScript.Shell; ");
 | 
			
		||||
        ps_cmd.push_str(&format!("$s = $ws.CreateShortcut('{}'); ", shortcut_path.to_string_lossy()));
 | 
			
		||||
        ps_cmd.push_str(&format!("$s.TargetPath = '{}'; ", target.to_string_lossy()));
 | 
			
		||||
        
 | 
			
		||||
        if let Some(icon_path) = icon {
 | 
			
		||||
            ps_cmd.push_str(&format!("$s.IconLocation = '{}'; ", icon_path.to_string_lossy()));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if let Some(desc) = description {
 | 
			
		||||
            ps_cmd.push_str(&format!("$s.Description = '{}'; ", desc));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        ps_cmd.push_str("$s.Save()");
 | 
			
		||||
        
 | 
			
		||||
        Command::new("powershell")
 | 
			
		||||
            .args(&["-Command", &ps_cmd])
 | 
			
		||||
            .output()?;
 | 
			
		||||
            
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #[cfg(unix)]
 | 
			
		||||
    {
 | 
			
		||||
        use std::fs::File;
 | 
			
		||||
        use std::io::Write;
 | 
			
		||||
        
 | 
			
		||||
        let desktop = if let Some(desktop_dir) = dirs::desktop_dir() {
 | 
			
		||||
            desktop_dir
 | 
			
		||||
        } else {
 | 
			
		||||
            return Err(anyhow::anyhow!("Could not find desktop directory"));
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        let shortcut_path = desktop.join(format!("{}.desktop", name));
 | 
			
		||||
        
 | 
			
		||||
        let mut file = File::create(&shortcut_path)?;
 | 
			
		||||
        
 | 
			
		||||
        writeln!(file, "[Desktop Entry]")?;
 | 
			
		||||
        writeln!(file, "Type=Application")?;
 | 
			
		||||
        writeln!(file, "Name={}", name)?;
 | 
			
		||||
        writeln!(file, "Exec={}", target.to_string_lossy())?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(icon_path) = icon {
 | 
			
		||||
            writeln!(file, "Icon={}", icon_path.to_string_lossy())?;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if let Some(desc) = description {
 | 
			
		||||
            writeln!(file, "Comment={}", desc)?;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        writeln!(file, "Terminal=false")?;
 | 
			
		||||
        
 | 
			
		||||
        // Make executable
 | 
			
		||||
        use std::os::unix::fs::PermissionsExt;
 | 
			
		||||
        let mut perms = std::fs::metadata(&shortcut_path)?.permissions();
 | 
			
		||||
        perms.set_mode(0o755);  // rwxr-xr-x
 | 
			
		||||
        std::fs::set_permissions(&shortcut_path, perms)?;
 | 
			
		||||
        
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user