From 7df4ceb1cf2a6edc058d28527140598ca90e1f10 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 11 Jul 2026 13:32:48 +0200 Subject: init --- src/gui.rs | 369 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 src/gui.rs (limited to 'src/gui.rs') diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..5fe1092 --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,369 @@ +use std::path::PathBuf; +use std::sync::mpsc; +use std::sync::mpsc::TryRecvError; +use std::sync::{Arc, Mutex}; +use std::thread; + +use egui::{Color32, ProgressBar, ScrollArea, TextEdit}; +use rfd::FileDialog; + +use crate::downloader::{self, Event}; + +pub struct App { + mod_url: String, + output_path: String, + target_version: String, + include_optional: bool, + + versions: Vec<(String, String)>, + loading_versions: bool, + version_handle: Option>>>>, + + downloading: bool, + finished: bool, + + downloads: Vec, + log_entries: Vec, + + receiver: Option>, +} + +struct DownloadState { + name: String, + total_bytes: u64, + downloaded_bytes: u64, + complete: bool, + error: Option, +} + +struct LogEntry { + text: String, + is_error: bool, +} + +impl App { + pub fn new() -> Self { + Self { + mod_url: String::new(), + output_path: std::env::current_dir() + .map(|p| p.display().to_string()) + .unwrap_or_default(), + target_version: String::new(), + include_optional: false, + versions: Vec::new(), + loading_versions: false, + version_handle: None, + downloading: false, + finished: false, + downloads: Vec::new(), + log_entries: Vec::new(), + receiver: None, + } + } + + fn log(&mut self, text: String, is_error: bool) { + self.log_entries.push(LogEntry { text, is_error }); + if self.log_entries.len() > 500 { + self.log_entries.remove(0); + } + } + + fn find_or_create_download(&mut self, name: &str) -> &mut DownloadState { + if let Some(pos) = self.downloads.iter().position(|d| d.name == name) { + &mut self.downloads[pos] + } else { + self.downloads.push(DownloadState { + name: name.to_string(), + total_bytes: 0, + downloaded_bytes: 0, + complete: false, + error: None, + }); + self.downloads.last_mut().unwrap() + } + } + + fn poll_events(&mut self) { + let mut events = Vec::new(); + if let Some(ref rx) = self.receiver { + loop { + match rx.try_recv() { + Ok(event) => events.push(event), + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => { + self.downloading = false; + self.receiver = None; + break; + } + } + } + } + for event in events { + self.handle_event(event); + } + } + + fn handle_event(&mut self, event: Event) { + match event { + Event::Log(text) => self.log(text, false), + Event::DownloadStart { name, total_bytes } => { + self.log(format!("Downloading: {name}"), false); + let dl = self.find_or_create_download(&name); + dl.total_bytes = total_bytes; + dl.downloaded_bytes = 0; + dl.complete = false; + dl.error = None; + } + Event::DownloadProgress { name, bytes } => { + let dl = self.find_or_create_download(&name); + dl.downloaded_bytes = bytes; + } + Event::DownloadComplete { name } => { + let dl = self.find_or_create_download(&name); + dl.complete = true; + dl.downloaded_bytes = dl.total_bytes; + self.log(format!("Downloaded: {name}"), false); + } + Event::DownloadError { name, error } => { + let dl = self.find_or_create_download(&name); + dl.error = Some(error.clone()); + self.log(format!("Error: {name} - {error}"), true); + } + Event::Finished { success, message } => { + self.log(message.clone(), !success); + self.downloading = false; + self.finished = true; + self.receiver = None; + } + } + } + + fn start_download(&mut self) { + let mod_url = self.mod_url.trim().to_string(); + if mod_url.is_empty() { + return; + } + + let output = PathBuf::from(&self.output_path); + let target_version = if self.target_version.trim().is_empty() { + None + } else { + Some(self.target_version.trim().to_string()) + }; + let include_optional = self.include_optional; + + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + self.downloading = true; + self.finished = false; + self.downloads.clear(); + self.log_entries.clear(); + + self.log("Starting download...".into(), false); + + thread::spawn(move || { + downloader::run_gui_download(&mod_url, &output, target_version, include_optional, tx); + }); + } + + fn load_versions(&mut self) { + let mod_url = self.mod_url.trim().to_string(); + if mod_url.is_empty() { + return; + } + + self.loading_versions = true; + let result: Arc>>> = Arc::new(Mutex::new(None)); + self.version_handle = Some(result.clone()); + + thread::spawn(move || { + let name = downloader::extract_mod_name(&mod_url); + let versions = match name { + Ok(name) => downloader::fetch_versions(&name).ok().unwrap_or_default(), + Err(_) => Vec::new(), + }; + *result.lock().unwrap() = Some(versions); + }); + } + + fn browse_output(&mut self) { + if let Some(path) = FileDialog::new().pick_folder() { + self.output_path = path.display().to_string(); + } + } +} + +impl eframe::App for App { + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.poll_events(); + + // Check version loading result + if self.loading_versions { + let result = if let Some(ref handle) = self.version_handle { + handle.lock().ok().and_then(|mut g| g.take()) + } else { + None + }; + if let Some(versions) = result { + self.versions = versions; + self.loading_versions = false; + self.version_handle = None; + } + } + + if self.downloading { + ctx.request_repaint(); + } + } + + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + ui.heading("Factorio Mod Downloader"); + ui.separator(); + + // ── Input section ── + ui.horizontal(|ui| { + ui.label("Mod URL:"); + ui.add_sized( + [ui.available_width() - 10.0, 20.0], + TextEdit::singleline(&mut self.mod_url) + .hint_text("https://mods.factorio.com/mod/... or mod name"), + ); + }); + + ui.horizontal(|ui| { + ui.label("Output:"); + ui.add_sized( + [ui.available_width() - 80.0, 20.0], + TextEdit::singleline(&mut self.output_path).hint_text("Download directory"), + ); + if ui.button("Browse").clicked() { + self.browse_output(); + } + }); + + ui.horizontal(|ui| { + ui.label("Version:"); + let version_enabled = !self.downloading && !self.versions.is_empty(); + ui.add_enabled_ui(version_enabled, |ui| { + egui::ComboBox::from_id_salt("version_combo") + .width(200.0) + .selected_text(if self.target_version.is_empty() { + "Latest" + } else { + &self.target_version + }) + .show_ui(ui, |ui| { + if ui.selectable_label(false, "Latest").clicked() { + self.target_version.clear(); + } + for (ver, label) in &self.versions { + if ui.selectable_label(self.target_version == *ver, label).clicked() { + self.target_version = ver.clone(); + } + } + }); + }); + + if ui + .add_enabled( + !self.downloading && !self.loading_versions, + egui::Button::new(if self.loading_versions { + "Loading..." + } else { + "Load Versions" + }), + ) + .clicked() + { + self.load_versions(); + } + + ui.checkbox(&mut self.include_optional, "Include optional deps"); + }); + + ui.add_space(8.0); + + // ── Download button ── + let can_download = !self.downloading + && !self.mod_url.trim().is_empty() + && !self.output_path.is_empty(); + + if ui + .add_enabled( + can_download, + egui::Button::new(if self.downloading { + "Downloading..." + } else if self.finished { + "Download Again" + } else { + "Start Download" + }), + ) + .clicked() + { + self.start_download(); + } + + ui.add_space(8.0); + + // ── Progress section ── + if !self.downloads.is_empty() { + egui::CollapsingHeader::new("Downloads") + .default_open(true) + .show(ui, |ui| { + for dl in &self.downloads { + let percentage = if dl.total_bytes > 0 { + dl.downloaded_bytes as f32 / dl.total_bytes as f32 + } else { + 0.0 + }; + + ui.horizontal(|ui| { + let status = if dl.complete { + "[OK]" + } else if dl.error.is_some() { + "[ERR]" + } else { + "[>>]" + }; + ui.label(format!("{status} {}", dl.name)); + + if let Some(ref err) = dl.error { + ui.colored_label(Color32::RED, format!("Error: {err}")); + } else if dl.total_bytes > 0 { + ui.label(format!( + "{:.1} / {:.1} MB", + dl.downloaded_bytes as f64 / 1_048_576.0, + dl.total_bytes as f64 / 1_048_576.0 + )); + } + }); + + let bar = + ProgressBar::new(percentage).desired_width(ui.available_width()); + ui.add(bar); + } + }); + } + + ui.add_space(8.0); + + // ── Log section ── + egui::CollapsingHeader::new("Log") + .default_open(true) + .show(ui, |ui| { + ScrollArea::vertical() + .max_height(300.0) + .stick_to_bottom(true) + .show(ui, |ui| { + for entry in &self.log_entries { + if entry.is_error { + ui.colored_label(Color32::RED, &entry.text); + } else { + ui.label(&entry.text); + } + } + }); + }); + } +} -- cgit v1.2.3