diff --git a/Cargo.lock b/Cargo.lock index d8d09f4..c5d9e5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,23 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -200,11 +217,12 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "9f2776ead772134d55b62dd45e59a79e21612d85d0af729b8b7d3967d601a62a" dependencies = [ "concurrent-queue", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -1196,6 +1214,7 @@ dependencies = [ "platform-dirs", "progress-streams", "regex", + "rfd", "rodio", "serde", "serde-xml-rs", @@ -3013,6 +3032,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -3344,6 +3369,29 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + [[package]] name = "ring" version = "0.17.8" @@ -3991,7 +4039,9 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", + "signal-hook-registry", "socket2", + "tracing", "windows-sys 0.48.0", ] @@ -4241,8 +4291,15 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "version" version = "3.0.0" @@ -5132,6 +5189,7 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", + "tokio", "tracing", "uds_windows", "windows-sys 0.52.0", @@ -5239,6 +5297,7 @@ dependencies = [ "enumflags2", "serde", "static_assertions", + "url", "zvariant_derive", ] diff --git a/Cargo.toml b/Cargo.toml index d214287..ecd23d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,10 @@ espim = "0.2" flate2 = "1.0" fs_extra = "1.3.0" futures = "0.3" -iced_aw = { git = "https://github.com/iced-rs/iced_aw.git", rev = "9ed46bffd0d819f22e07db6c282fbef733007df5", default-features = false, features = ["tabs", "icons"] } +iced_aw = { git = "https://github.com/iced-rs/iced_aw.git", rev = "9ed46bffd0d819f22e07db6c282fbef733007df5", default-features = false, features = [ + "tabs", + "icons", +] } lazy_static = "1.5" log = { version = "0.4.22", features = ["std"] } open = "5" @@ -41,17 +44,29 @@ time = "0.3" tokio = { version = "1", default-features = false, features = ["fs"] } ureq = { version = "2.9", default-features = false, features = ["json", "tls"] } version = "3" -zip-extract = { version = "0.1.3", default-features = false, features = ["deflate"] } +zip-extract = { version = "0.1.3", default-features = false, features = [ + "deflate", +] } +rfd = { version = "0.14.1", default-features = false, features = [ + "tokio", + "xdg-portal", +] } [dependencies.iced] version = "0.12.1" default-features = false -features = ["web-colors", "advanced", "image", "tokio"] +features = [ + "web-colors", + "advanced", + "image", + "tokio", + "wgpu", # tiny-skia backend panics with text in rows: https://github.com/iced-rs/iced/issues/2332 +] [target.'cfg(windows)'.build-dependencies] winres = "0.1" [profile.release] strip = true -opt-level = 'z' # Optimize for size. +opt-level = 'z' # Optimize for size. lto = true diff --git a/src/install_frame.rs b/src/install_frame.rs index 7926e29..6fd0516 100644 --- a/src/install_frame.rs +++ b/src/install_frame.rs @@ -1,4 +1,5 @@ use crate::instance::{get_instances_dir, InstanceType}; +use crate::settings::Settings; use crate::style::text_button; use crate::{instance, Message}; use core::fmt; @@ -57,10 +58,18 @@ impl fmt::Display for InstanceSourceType { } impl InstallFrame { - pub fn update(&mut self, message: InstallFrameMessage) -> Command { + pub fn update( + &mut self, + message: InstallFrameMessage, + settings: &mut Settings, + ) -> Command { match message { InstallFrameMessage::StartInstallation(instance_type) => { - if let Some(mut destination) = get_instances_dir() { + if let Some(mut destination) = if settings.use_custom_install_dir { + settings.custom_install_dir.clone() + } else { + get_instances_dir() + } { destination.push(&self.name); return Command::perform( instance::perform_install( diff --git a/src/main.rs b/src/main.rs index 93e117d..236f40c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -157,7 +157,9 @@ impl Application for ESLauncher { fn update(&mut self, message: Self::Message) -> Command { match message { - Message::InstallFrameMessage(msg) => return self.install_frame.update(msg), + Message::InstallFrameMessage(msg) => { + return self.install_frame.update(msg, &mut self.settings) + } Message::InstanceMessage(name, msg) => { match self.instances_frame.instances.get_mut(&name) { None => error!("Failed to find internal Instance with name {}", &name), @@ -174,7 +176,7 @@ impl Application for ESLauncher { } } } - Message::SettingsMessage(msg) => self.settings.update(msg), + Message::SettingsMessage(msg) => return self.settings.update(msg), Message::AddInstance(instance) => { let is_ready = instance.state.is_ready(); self.instances_frame @@ -197,9 +199,7 @@ impl Application for ESLauncher { MusicCommand::Play => MusicState::Playing, _ => self.settings.music_state, }; - if let Err(e) = self.settings.save() { - error!("Failed to save settings.json: {:#?}", e); - }; + self.settings.save(); } Message::TabSelected(active_tab) => self.active_tab = active_tab, Message::PluginFrameLoaded(plugins) => { diff --git a/src/settings.rs b/src/settings.rs index 46efdc5..6c2f035 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,38 +1,64 @@ use crate::music::MusicState; -use crate::{get_data_dir, Message}; +use crate::{get_data_dir, style, Message}; use anyhow::{Context, Result}; -use iced::widget::{Checkbox, Column, Container, Row, Space, Text}; -use iced::Length; +use iced::advanced::graphics::core::Element; +use iced::widget::text_input::StyleSheet; +use iced::widget::{checkbox, container, row, text, Text}; +use iced::{ + widget::{button, Column, Container, Row}, + Length, +}; +use iced::{Alignment, Command, Padding, Renderer}; use serde::{Deserialize, Serialize}; -use std::fs::File; +use std::{fs::File, path::PathBuf}; + +#[derive(Clone, Debug)] +pub enum CustomInstallPath { + SetEnabled(bool), + RequestPath, + SetPath(PathBuf), +} #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Settings { pub music_state: MusicState, pub dark_theme: bool, + pub custom_install_dir: Option, + #[serde(default)] + pub use_custom_install_dir: bool, } #[derive(Debug, Clone)] pub enum SettingsMessage { DarkTheme(bool), + CustomInstallPath(CustomInstallPath), } -impl Settings { +impl Default for Settings { fn default() -> Self { Self { - music_state: MusicState::default(), + music_state: Default::default(), dark_theme: dark_light::detect().eq(&dark_light::Mode::Dark), + custom_install_dir: Default::default(), + use_custom_install_dir: Default::default(), } } +} - pub fn save(&self) -> Result<()> { - let mut settings_file = - get_data_dir().ok_or_else(|| anyhow!("Failed to get app save dir"))?; - settings_file.push("settings.json"); +impl Settings { + pub fn save(&self) { + let save = || -> Result<()> { + let mut settings_file = + get_data_dir().ok_or_else(|| anyhow!("Failed to get app save dir"))?; + settings_file.push("settings.json"); - let file = File::create(settings_file)?; - serde_json::to_writer_pretty(file, self)?; - Ok(()) + let file = File::create(settings_file)?; + serde_json::to_writer_pretty(file, self)?; + Ok(()) + }; + if let Err(e) = save() { + error!("Failed to save settings.json: {:#?}", e); + } } pub fn load() -> Self { @@ -59,29 +85,125 @@ impl Settings { } pub fn view(&self) -> Container { - let settings_row = |label, content| { - Row::new() - .push(Text::new(label)) - .push(Space::with_width(Length::Fill)) - .push(content) - }; - + fn settings_row<'a>( + label: &'a str, + content: impl Into>, + enabled: bool, + ) -> impl Into> { + let setting_spacer = || { + iced::widget::horizontal_rule(2).style(iced::theme::Rule::from( + |theme: &iced::Theme| { + let mut appearance = + iced::widget::rule::StyleSheet::appearance(theme, &Default::default()); + appearance.color.a *= 0.75; + appearance + }, + )) + }; + let container = container( + Column::new() + .push(setting_spacer()) + .push( + Row::new() + .push(Text::new(label)) + .push( + container(content) + .align_x(iced::alignment::Horizontal::Right) + .width(Length::Fill), + ) + .align_items(Alignment::Center), + ) + .spacing(10.0), + ); + if enabled { + container + } else { + container.style(iced::theme::Container::Custom(Box::new( + |theme: &iced::Theme| container::Appearance { + text_color: Some( + theme.disabled(&iced::theme::TextInput::Default).icon_color, + ), + ..Default::default() + }, + ))) + } + } + let btn = button(style::folder_icon().size(12.0)) + .on_press_maybe( + self.use_custom_install_dir + .then_some(Message::SettingsMessage( + SettingsMessage::CustomInstallPath(CustomInstallPath::RequestPath), + )), + ) + // .style(icon_button()) + .padding(Padding::from([2, 0])); Container::new( - Column::new().push(settings_row( - "Dark Theme", - Checkbox::new("", self.dark_theme) - .on_toggle(|v| Message::SettingsMessage(SettingsMessage::DarkTheme(v))), - )), + Column::new() + .push(settings_row( + "Use custom install directory", + checkbox("", self.use_custom_install_dir).on_toggle(|f| { + Message::SettingsMessage(SettingsMessage::CustomInstallPath( + CustomInstallPath::SetEnabled(f), + )) + }), + true, + )) + .push(settings_row( + "Custom install directory", + row!( + text(format!( + "Installing to {}", + self.custom_install_dir + .clone() + .unwrap_or_default() + .to_string_lossy() + .as_ref() + )) + .size(12.0), + btn, + ) + .align_items(Alignment::Center) + .spacing(10.0) + .padding(Padding { + top: 0.0, + right: 10.0, + bottom: 0.0, + left: 0.0, + }), + self.use_custom_install_dir, + )) + .spacing(10.0), ) - .padding(100.) + .padding(100.0) } - pub fn update(&mut self, message: SettingsMessage) { + pub fn update(&mut self, message: SettingsMessage) -> Command { match message { - SettingsMessage::DarkTheme(bool) => self.dark_theme = bool, - }; - if let Err(e) = self.save() { - error!("Failed to save settings.json: {:#?}", e) + SettingsMessage::CustomInstallPath(custom_install_path) => match custom_install_path { + CustomInstallPath::RequestPath => { + return Command::perform( + rfd::AsyncFileDialog::new().pick_folder(), + |f| match f { + Some(handle) => { + Message::SettingsMessage(SettingsMessage::CustomInstallPath( + CustomInstallPath::SetPath(handle.path().to_path_buf()), + )) + } + None => Message::Dummy(()), + }, + ) + } + CustomInstallPath::SetEnabled(f) => { + self.use_custom_install_dir = f; + } + CustomInstallPath::SetPath(p) => { + self.custom_install_dir = Some(p); + } + }, + SettingsMessage::DarkTheme(dark) => self.dark_theme = dark, }; + self.save(); + + Command::none() } }