diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 75432b5a..b3aef5fa 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -11,11 +11,12 @@ use std::ops::Deref; use std::time::{Duration, SystemTime}; use std::{ collections::{HashMap, HashSet}, + fs, ops::DerefMut, path::PathBuf, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use eframe::egui::{Button, CollapsingHeader, RichText, Visuals}; use eframe::epaint::{Pos2, Vec2}; use eframe::{ @@ -34,8 +35,9 @@ use tracing::{debug, trace}; use crate::mod_lints::{LintId, LintReport, SplitAssetPair}; use crate::Dirs; use crate::{ + clear_directory, integrate::uninstall, - is_drg_pak, + is_drg_pak, is_valid_directory, providers::{ ApprovalStatus, FetchProgress, ModInfo, ModSpecification, ModStore, ProviderFactory, }, @@ -929,26 +931,26 @@ impl App { }); ui.end_row(); - let config_dir = &self.state.dirs.config_dir; - ui.label("Config directory:"); - if ui.link(config_dir.display().to_string()).clicked() { - opener::open(config_dir).ok(); - } - ui.end_row(); + let edit_directory_field = |ui: &mut egui::Ui, label: &str, path: &mut String, err: &mut Option| { + ui.label(label); + ui.horizontal(|ui| { + ui.text_edit_singleline(path); - let cache_dir = &self.state.dirs.cache_dir; - ui.label("Cache directory:"); - if ui.link(cache_dir.display().to_string()).clicked() { - opener::open(cache_dir).ok(); - } - ui.end_row(); + if ui.button("Browse").clicked() { + if let Some(selected_path) = rfd::FileDialog::new().pick_folder() { + *path = selected_path.to_string_lossy().to_string(); + *err = None; + } + } - let data_dir = &self.state.dirs.data_dir; - ui.label("Data directory:"); - if ui.link(data_dir.display().to_string()).clicked() { - opener::open(data_dir).ok(); - } - ui.end_row(); + if let Some(err_msg) = err { + ui.label(&*err_msg); + } + ui.end_row(); + }); + }; + + edit_directory_field(ui, "Cache Directory:", &mut window.cache_dir, &mut window.cache_dir_err); ui.label("GUI theme:"); ui.horizontal(|ui| { @@ -981,24 +983,56 @@ impl App { } }); - ui.with_layout(egui::Layout::right_to_left(Align::TOP), |ui| { - if ui.add_enabled(window.drg_pak_path_err.is_none(), egui::Button::new("save")).clicked() { + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + let can_save = window.drg_pak_path_err.is_none() + && window.cache_dir_err.is_none(); + + if ui.add_enabled(can_save, egui::Button::new("save")).clicked() { try_save = true; } + if let Some(error) = &window.drg_pak_path_err { ui.colored_label(ui.visuals().error_fg_color, error); } + if let Some(error) = &window.cache_dir_err { + ui.colored_label(ui.visuals().error_fg_color, error); + } }); }); if try_save { - if let Err(e) = is_drg_pak(&window.drg_pak_path).context("Is not valid DRG pak") { + let mut has_error = false; + + if let Err(e) = is_drg_pak(&window.drg_pak_path) { window.drg_pak_path_err = Some(e.to_string()); - } else { - self.state.config.drg_pak_path = Some(PathBuf::from( - self.settings_window.take().unwrap().drg_pak_path, - )); - self.state.config.save().unwrap(); + has_error = true; + } + if let Err(e) = is_valid_directory(&window.cache_dir) { + window.cache_dir_err = Some(e); + has_error = true; + } + + if !has_error { + if let Some(old_cache_dir) = &self.state.config.cache_dir { + if old_cache_dir.to_string_lossy() != window.cache_dir { + if let Err(e) = fs::create_dir_all(&window.cache_dir) { + window.cache_dir_err = + Some(format!("Failed to create new cache directory: {}", e)); + has_error = true; + } else if let Err(e) = clear_directory(old_cache_dir) { + window.cache_dir_err = + Some(format!("Failed to clear cache directory: {}", e)); + has_error = true; + } + } + } + + if !has_error { + self.state.config.drg_pak_path = Some(PathBuf::from(&window.drg_pak_path)); + self.state.config.cache_dir = Some(PathBuf::from(&window.cache_dir)); + + self.state.config.save().unwrap(); + } } } else if !open { self.settings_window = None; @@ -1484,19 +1518,27 @@ impl WindowProviderParameters { struct WindowSettings { drg_pak_path: String, drg_pak_path_err: Option, + cache_dir: String, + cache_dir_err: Option, } impl WindowSettings { fn new(state: &State) -> Self { - let path = state - .config - .drg_pak_path - .as_ref() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_default(); Self { - drg_pak_path: path, + drg_pak_path: state + .config + .drg_pak_path + .as_ref() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(), drg_pak_path_err: None, + cache_dir: state + .config + .cache_dir + .as_ref() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(), + cache_dir_err: None, } } } diff --git a/src/lib.rs b/src/lib.rs index 37ef9095..74a4ed4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,11 +7,13 @@ pub mod mod_lints; pub mod providers; pub mod state; +use std::fs::{self, copy, create_dir_all}; use std::io::{Cursor, Read}; use std::ops::Deref; use std::str::FromStr; use std::{ collections::HashSet, + io, path::{Path, PathBuf}, }; @@ -88,6 +90,63 @@ pub fn write_file, C: AsRef<[u8]>>(path: P, data: C) -> Result<() .with_context(|| format!("Could not write to file {}", path.as_ref().display())) } +pub fn is_valid_directory(path: &str) -> Result<(), String> { + let path = Path::new(path); + + if !path.exists() { + return Err("Path does not exist.".to_string()); + } + if !path.is_dir() { + return Err("Path is not a directory.".to_string()); + } + + match fs::metadata(path) { + Ok(metadata) => { + if !metadata.permissions().readonly() { + Ok(()) + } else { + Err("Directory is not writable.".to_string()) + } + } + Err(_) => Err("Unable to access directory metadata.".to_string()), + } +} + +pub fn copy_directory_contents(src: &Path, dest: &Path) -> io::Result<()> { + if src.is_dir() { + create_dir_all(dest)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dest_path = dest.join(entry.file_name()); + + if path.is_dir() { + copy_directory_contents(&path, &dest_path)?; + } else { + copy(&path, &dest_path)?; + } + } + } + Ok(()) +} + +pub fn clear_directory(path: &Path) -> io::Result<()> { + if path.is_dir() { + for entry in fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + fs::remove_dir_all(&path)?; + } else { + fs::remove_file(&path)?; + } + } + } + Ok(()) +} + pub fn is_drg_pak>(path: P) -> Result<()> { let mut reader = std::io::BufReader::new(open_file(path)?); let pak = repak::PakBuilder::new().reader(&mut reader)?; diff --git a/src/state/mod.rs b/src/state/mod.rs index 0fd01f56..c393d88c 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -337,6 +337,7 @@ pub struct Config { pub gui_theme: Option, #[serde(default, skip_serializing_if = "is_false")] pub disable_fix_exploding_gas: bool, + pub cache_dir: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -389,6 +390,8 @@ impl DerefMut for VersionAnnotatedConfig { impl Default for Config!["0.0.0"] { fn default() -> Self { + let default_dirs = Dirs::default_xdg().expect("Failed to get default directories"); + Self { provider_parameters: Default::default(), drg_pak_path: DRGInstallation::find() @@ -396,6 +399,7 @@ impl Default for Config!["0.0.0"] { .map(DRGInstallation::main_pak), gui_theme: None, disable_fix_exploding_gas: false, + cache_dir: Some(default_dirs.cache_dir), } } }